Juri Shutenko Personal Homepage. Visual FoxPro.

Заметки... II

Теперь уделим толику времени ошибкам, которых следует избегать...

Нетипичные ошибки программиста

Предусмотреть все и вся, наверное, невозможно. Представим себе ситуацию. Не очень дисциплинированный Пользователь работает на своем компе, у которого свойство Auto-hide панели Taskbar рабочего стола установлено. Работал, работал он с вашей программой, потом ущел куда-то вернулся и, поскольку Taskbar скрыт, пытается запустить вашу программу, дважды щелкнув по иконке на рабочем столе. В результате можем поиметь вторую инстанцию приложения с возможными в таком случае проблемами. Я отношу это к классу нетипичных ошибок, так как проверка существующей инстанции приложения должна присутствовать в программе (IMHO). Как быть? Есть несколько вариантов, которые имеют свои преимущества и недостатки.

Вариант 1. Поиск окна и вывод его наверх.

Для этого будем использовать 5 функций Windows API:

Первая функция возвращает handle активного окна и не требуеи параметров.
Вторая функция возвращает handle окна, зависящий от передаваемых параметров - handle окна, по относительным связям которого, определяемым вторым параметром, возвращается искомый.
Третья функция возвращает то, что указано в Title bar окна
Четвертая функция определяет, является-ли окно, определенное по его handle, видимым? Пятая должна бы установить окно поверх остальных.

Код в таком случае может выглядеть как приведено ниже, может быть оформлен в виде процедуры в главном файле и вызван как можно раньше, до открытия баз и прочего.

LOCAL lcWindowName, llIsWindowExist, lnAWhwnd, lcSWTitle, lnTextLen 
lcWindowName = <имя окна вашего приложения>

#DEFINE GW_OWNER 4
#DEFINE GW_HWNDFIRST 0
#DEFINE GW_HWNDNEXT 2

Declare integer GetActiveWindow In Win32API 
Declare integer GetWindow In Win32API Integer,Integer
Declare integer GetWindowText In Win32API Integer, String, Integer
Declare integer IsWindowVisible In Win32API Integer
Declare integer SetForegroundWindow In Win32API Long lnhWnd

llIsWindowExist = .F.
If LEN(lcWindowName) < 1
    Return llIsWindowExist
Endif
* для начала получим handle нашего окна
lnAWhwnd   = GetActiveWindow()

* далее получим handle первого открытого окна
lnhWndNext = GetWindow(lnAWhwnd,GW_HWNDFIRST) && get the handel of the first window

* далее просматриваем каждое окно, на предмет совпадения текста в Title bar
DO WHILE lnhWndNext <> 0
    * если окно не наше и не является дочерним
    IF (lnhWndNext <> lnAWhwnd .AND. GetWindow(lnhWndNext,GW_OWNER) = 0)
        lcSWTitle = SPACE(64)
        lnTextLen = GetWindowText(lnhWndNext,@lcSWTitle,64)
        IF lcWindowName $ lcSWTitle
            llIsWindowExist = .T.
            * здесь ваши варианты, или как пример
            * пробуем вытащить окно наверх 
            =SetForegroundWindow(lnhWndNext)
            EXIT
        ENDIF
    ENDIF
    lnhWndNext = GetWindow(lnhWndNext,GW_HWNDNEXT)
ENDDO
* и возвращаем результат
RETURN llIsWindowExist

Это работающий код при некоторых оговорках. Подразумевается, что вы в ходе программы не меняете полностью наименования, которое показывается в Title bar. Я, к примеру, достаточно часто меняю, в резульате чего имя окна может иметь наименование, отражающее текущую операцию выполняемую пользователем и имя самого пользователя. Можно, конечно оставить аббревиатуру в качестве обязательного элемента наименования и в строке кода:

IF lcWindowName $ lcSWTitle

проверять наличие этого сокращения. Далее, могут наложиться ограничения, связанные с использованием функции SetForegroundWindow (смотри ссылку в перечне функций). В таком случае можно исследовать, что она вернула. Если ноль - значит не может вытащить окно наверх, если нет- значит удалось. В случае нуля можно было бы вывести MESSAGEBOX, но это тоже не лучшее решение.

Две вариации на тему окна с именем от Mike Gagnon - с использованием функций WinAPI

Procedure DupStart
#DEFINE SW_NORMAL    1
#DEFINE SW_MAXIMIZE  3
#DEFINE SW_MINIMIZE  6

DECLARE Long FindWindow in Win32API String, String
DECLARE Long BringWindowToTop in Win32API Long
DECLARE Long ShowWindow in Win32API Long, Long

Local lnHWND,lcTitle
lcTitle = _screen.Caption
_screen.Caption = Sys(3)
lnHWND = FindWindow(null,m.lcTitle)
_Screen.Caption = m.lcTitle

IF m.lnHWND >0
  BringWindowToTop(m.lnHWND)
  ShowWindow(m.lnHWND,SW_MAXIMIZE)
  QUIT
ENDIF
ENDPROC

и с использованием FoxTools:

SET LIBRARY TO SYS(2004)+'foxtools.fll' ADDITIVE
   * FindWindow() takes two parameters and returns the handle of the
   * window, HWND, if it was found.
   GetWind = RegFn('FindWindow', 'CC', 'I')
   *Set the first parameter of the call to getwind to 0, null.
   wclass=0
   winname='My Application'
   apphand=CallFn(GetWind,wclass ,winname)
   *If the call was successful, stop processing.
   IF apphand<>0
      WAIT WINDOW ;
        'You cannot start another instance of the window 'My Application'!'
      QUIT
   ENDIF
   MODIFY WINDOW screen TITLE 'My Application'
   WAIT WINDOW ;
     'The first instance of the window 'My Application' is running.'

Вариант 2. Использование Atom'a.

Ниже приведен заимствованный фрагмент кода с сохраненными комментариями автора.

** declare the windos api functions
declare integer GlobalFindAtom in win32api string
declare integer GlobalAddAtom in win32api string
declare integer GlobalDeleteAtom in win32api integer

* need a location to hold the atom for exiting. made public
* here, could be a property of a class.

public natom

** when creating a name for the atom 
** it has nothing to do with the window name.  
** so make the name anything.  
** it is not case senstive abc = ABC

** if the atom exist from previous load it will return a 
** positive number

if GlobalFindAtom('any name') > 0
   quit 
else
   natom = GlobalAddAtom('any name')  && create atom for the app
endif

** I say use any name, but the name must be consistant.  
** doing a find and add atom will be with the same name.
** at the end of the program just before exiting
** destroy the atom

GlobalDeleteAtom(natom)

** it is important to destroy the atom, 
** for it is not automaticly destroyed on exit.
** which will mean that the program will not run again, 
** untill the system is restarted.

quit

Ниже приведен фрагмент кода от Mike Gagnon, c исправленными мною опечатками, что отнюдь не умаляет достоинств автора

#Define AtomStrLength   512
Local lcAtomName
lcAtomName = "mon application"
Declare Integer GlobalAddAtom In win32api String
Declare Integer GlobalDeleteAtom In win32api Integer
Declare Integer GlobalGetAtomName In kernel32;
     INTEGER  nAtom,;
     STRING @ lpBuffer,;
     INTEGER  nSize
findAtom(lcAtomName)
Function findAtom(tcAtom)
Create Cursor cs (atom N(12), strlen N(5), Name C(100))
Index On Allt(Name) Tag Name
For nAtom = 49152 To 65535
     lpBuffer = Repli(Chr(0), AtomStrLength)
     lnResult = GlobalGetAtomName (nAtom, @lpBuffer, AtomStrLength)

     If lnResult > 0
          Insert Into cs Values (nAtom, lnResult, Left(lpBuffer, lnResult))
     Endif
Endfor
Select cs
If Seek(lcAtomName)
     Messagebox("Application is already running")
     Quit
Else
     = GlobalAddAtom(lcAtomName)
Endif

Вариант 3. Использование блоков взаимного исключения - Mutex.

Объект Mutex придуман для синхронизации между процессами и его смысл заключается в том, что его можно "захватить" и пока он захвачен, другой аналогичный процесс уже не может перехватить его и будет дожидаться его освобождения другим процессом. Впрочем, другой процесс может получить информацию о состоянии mutex'a и в случае если он занят вызвать процедуру, которая прекратит его самого. Тоже вроде неплохой вариант. Как его реализовать? Для создания этого объекта используется функция WinAPI CreateMutex. Функция принимает три параметра, первый из которых определяет, может-ли объект mutex быть наследуемым (игнорируется и должен быть нулем), второй определяет, кто является первоначальным собственником объекта и третий - имя, в случае, если мы создаем именованный объект. Подробно описано выше, в ссылке, под именем функции. Итак, для наших целей используем именованный объект. Чтобы уникально именовать наш объект, воспользуемся Globally Unique Identifier' ом (GUID), который может быть получен двумя строками кода (благодарность Mike Gagnon):

loGUID = CreateObject("scriptlet.typelib")
lcGUID = Substr(loGUID.GUID, 2, 36)

Получите с помощью указанного выше кода пару идентификаторов и пропишите их в своей главной программе, ну скажем так:

lcGUIDForMutex  ="4C45A74D-2841-4730-BFE0-D2A4CD68F1E8"
lcGUIDForScreen ="5D1BD013-C60D-41A8-9056-5AC58EE825D1"

Нам потребуются несколько WinAPI функций:

Ссылки на описания приведены под именами функций

Код с использованием объекта Mutex приведен ниже (заимствован - извиняюсь, не знаю имени автора- и немного переработан)

...
lcGUIDForMutex  ="4C45A74D-2841-4730-BFE0-D2A4CD68F1E8"
lcGUIDForScreen ="5D1BD013-C60D-41A8-9056-5AC58EE825D1"

IF udf_IsPrevInstanceExist()
    Do udp_Shutdown
ENDIF
ON Shutdown Do udp_Shutdown
...
* здесь код вашей главной программы
...


FUNCTION udf_IsPrevInstanceExist()

   #DEFINE ERROR_ALREADY_EXISTS  183
   #DEFINE GW_CHILD                5
   #DEFINE GW_HWNDNEXT             2
   #DEFINE SW_MAXIMIZE             3
   #DEFINE SW_NORMAL               1

   LOCAL lnhMutex, lnHwnd, llReturn

   DO udp_DeclareMutexAPI

   lnhMutex = CreateMutex(0, 1, lcGUIDForMutex)
    IF GetLastError()= ERROR_ALREADY_EXISTS
        DO udp_DeclareWindowsAPI()
        llReturn = .T.
        lnHwnd = GetWindow(GetDesktopWindow(), GW_CHILD)
        DO WHILE lnHwnd != 0 && loop through all windows
            IF GetProp(lnHwnd, lcGUIDForScreen) = 1
                BringWindowToTop(lnHwnd)
                ShowWindow(lnHwnd,SW_NORMAL)
*                или открываем на весь экран
*                ShowWindow(lnHwnd,SW_MAXIMIZE)
                llReturn = .T.
                EXIT
            ENDIF
            lnHwnd = GetWindow(lnHwnd, GW_HWNDNEXT)
        ENDDO
        CloseHandle(lnHwnd)
        CloseHandle(lnhMutex)
        CLEAR DLLS "BringWindowToTop", "GetDesktopWindow", ;
                    "GetProp", "GetWindow", "ShowWindow", ;
                    "CloseHandle"
    ELSE
        =SetProp(_VFP.HWND, lcGUIDForScreen, 1)
        _screen.AddProperty("MutexHandle",lnhMutex)
        llReturn = .F.
    ENDIF
    CLEAR DLLS "CreateMutex", "GetLastError", "SetProp"
    RETURN (llReturn)
ENDFUNC

PROCEDURE udp_DeclareMutexAPI()
   Declare integer CloseHandle In Kernel32 Integer hObject 
   Declare integer CreateMutex  In Win32API ;
      Integer lpMutexAttributes, Integer bInitialOwner, String lpName
   Declare integer GetLastError In Win32API
   Declare integer  SetProp In User32 Integer hWnd, String lpString, Integer hData
ENDPROC

PROCEDURE udp_DeclareWindowsAPI()
    Declare integer BringWindowToTop In Win32API Integer hWnd
    Declare integer GetDesktopWindow In User32
    Declare integer GetProp In User32 Integer hWnd, String  lpString
    Declare integer GetWindow In User32 Integer hWnd, Integer uCmd
    Declare integer ShowWindow In Win32API Integer hWnd, Integer nCmdShow
ENDPROC

PROCEDURE udp_Shutdown()
    ON Shutdown
    If PEMSTATUS(_screen,"MutexHandle",5) 
        Declare integer ReleaseMutex IN Win32API Integer hMutex
        Declare integer CloseHandle IN Kernel32 Integer hObject
        ReleaseMutex(_screen.MutexHandle)
        CloseHandle(_screen.MutexHandle)
        CLEAR DLLS "ReleaseMutex", "CloseHandle"
    Endif
    QUIT
ENDPROC

А что, если ключ к вашей программе выведен в панель Quick Launch и Пользователь производит на нем двойной щелчок? Поговорим об этом позже, когда будем обсуждать достоинства и недостатки того или способа определения запущенного приложения.

Cелектор для быстрого перехода на сайты, связанные с Visual FoxPro.