gbInstanceManager is a demonstration of how to manage instances within a PowerBASIC application. Limiting the number of instances, synchronizing menus, and closing selected instances are a few of the key features. The code does not lend itself to a single include file for incorporating the features into an application, but is modular enough that adding it to an application is quite easy.
Download (v1.0, 40K) |
gbInstanceManager is a demonstration of managing instances, so there is no real content to the main screen - just a single toolbar with a button dropdown menu which displays a menu that shows actions and settings a user may make.
Here's the main window, with several instances displayed:
Here's the main window, with the various menu options dislayed.
The dropdown menu is discussed below in the section on the Toolbar.
Also, see the Programmers Notes below for information on source code that provides useful features that may not be on this list.
The following options are available on the toolbar.
Settings
Pressing the Settings button will open a new instance, as constraing by the
instance settings. The dropdown arrow to the right of the Setting button is
discussed in the next section.
Help
Displays this online help page in the user's default browser client.
Ask
Forces gbInstanceManager to ask the user each time a new instance is opened.
Ask/AutoOpen/AutoSwitch are mutually exclusive settings.
AutoOpen
Opens a new instance, up to the maximum number of instances allowed, without
asking the user for approval.
Ask/AutoOpen/AutoSwitch are mutually exclusive settings.
AutoSwitch
Prevents the user from opening a new instance. Instead, displays and gives focus
to the most recent instance.
Ask/AutoOpen/AutoSwitch are mutually exclusive settings.
TimeStamp Instances
Adds, to the caption, the time an instance was started. Changing this setting
changes all existing/future instances.
Close All
Closes all instances, include the instance in which the command was selected.
Close All Other
Closes all instances, except for the instance in which the command was selected.
Open New Instance
Attempts to open new instance, subject to the settings above. This is equivalent
to double-clicking the application shortcut or double-clicking on the application
in Windows Explorer.
Set Max Instance Count
By default, only 5 instances are allowed by gbInstanceManager. Use this option
to change the default.
Existence of Previous Instances
Identifying whether a previous instance of an application exists is done
using mutexes. This line of code shows the basic approach:
If CreateMutex(ByVal %Null, 0, UniqueName) = 0 Or _ GetLastError = %ERROR_ALREADY_EXISTS Then
See the AllowNetInstance procedure for more details.
Identifying Which WIndows are Instances
With SDK window creation, the class name of the windows can be set by the
code - making it easy to find instances of an application. But since all
DDT dialogs carry the same #32770 class name, a different mechanism is needed
locate all instances of an application.
gbInstanceManager takes the approach of using SetWindowLong() to assign an arbitrary LONG number to all instances. With 4G numbers to choose from, the odds of another app also assigning the same user value is acceptably low.
Here are the lines of code that support the assignment:
%UniqueNumber = 1818181818 SetWindowLong(hDlg, %GWL_USERDATA, %UniqueNumber)
Synchronizing Menu Options
gbInstanceManager shows how to synchronize menu settings, in real-time, across all
instances. The approach taken is to register a custom Windows message and use that
message to send settings information to all instances. Instances which receive the
message will update their menu settings to match.
Here are the lines of code that support registering the custom message:
$CustomMsg = "InstanceSetting" hMsg = RegisterWindowMessage($CustomMsg)
Custom messages, like any message, allows sending of two LONG values. The comments in the code below describe the values sent with the custom message. The Select Case statements should the actual code that is run when a custom message is received. See the dialog callback function for more details.
Case hMsg '0,hWin hWin is the sending instance '1,n n is maxinstancecount '2,n n is instance setting Select Case Cb.WParam Case 0 : If hDlg <> Cb.LParam Then Dialog End hDlg Case 1 : MaxInstanceCount = Cb.LParam Case 2 : InstanceSetting = Cb.LParam : SetMenuStatus Case 3 : TimeStamp = Cb.LParam : SetMenuStatus End Select
Session Information
The location and size of the last opened instance is saved in an INI file. When a
new instance is created, it reads the INI file and opens in a staggered
position from the last instance.
These two lines of code are used to save/restore dialog location and size. They both call the Settings_INI procedure, which serves to save and restore dialog location and size.
Settings_INI "get" Settings_INI "save"
The "get" of location/size information is done only once - when the instance is started.
The "save" of location/size information, for the active instance, occurs with three events:
- Close All Other
- New Instance
- WM_Destroy
See the Settings_INI procedure for more details.
Options Management
The INI file is also used to store the Ask/AutoOpen/AutoSwitch and TimeStamp
settings. Because it is necessary to save these settings at the moment an
instance changes the settings (so the instance can synchronize its menu settings),
the Settings_INI "get"/"save" code is not used. Two custom procedures,
GetInstanceSettings and SaveInstanceSettings are used instead. These only modify
the INI to update the properties just mentioned.
GetInstanceSettings is used once during a session, when creating a new instance.
SaveInstanceSettings is used each time the Ask/AutoOpen/AutoSwitch or TimeStamp settings are changed in an instance. That makes the values available to future instances.
Identifying Most Recent Instance
To identify the most recent instance handle (opened when AutoSwitch or Ask/No are used)
a loop is used with GetWindow() and GetWindowLong() to identify windows whose
user data match the unique number (1818181818 is used in gbInstanceManager).
When a matching window is found, the GetWindowThreadProcessID() and OpenProcess()
are used to get the creation time of the application. The CompareFileTime() API is
used to identify the most recent instance creation time for use in identifying
the most recent instance.
'get the most recent matching instance, limit # instances hTry = GetForegroundWindow() Do While hTry If GetWindowLong(hTry, %GWL_UserData) = %UniqueNumber Then Incr InstanceCount If InstanceCount >= MaxInstanceCount Then ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(SBProc), GetModuleHandle(""), GetCurrentThreadId) MsgBox "No more instances allowed.", %MB_Ok + %MB_IconInformation, "Open New Instance" Beep : Function = 0 : Exit Function '<--- # instances exceeds max count End If 'get hWin creation time GetWindowThreadProcessID(hTry,PID) hProcess = OpenProcess(%Process_Query_Information Or %Process_VM_Read, %False, PID) If hProcess Then GetProcessTimes(hProcess, CreationTime, ExitTime, KernelTime, UserTime) If CompareFileTime(CreationTime, MostRecentFT) = 1 Then MostRecentFT = CreationTime hMostRecent = hTry 'later, focus will be given to this instance End If End If End If hTry = GetWindow(hTry, %GW_HWNDNEXT) Loop
Small InputBox$
The problem with a standard input box it that it is so ugly! The programmer has little
control over what it looks like. But with the following code, you can make it look much
nicer, like this:
Call InputBox$ like this:
ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(InputBoxProcExt), GetModuleHandle(""), GetCurrentThreadId) temp$ = InputBox$("", "Max Instance Count", Str$(MaxInstanceCount),w+40,h+150)
with this supporting procedure:
Function InputBoxProcExt(ByVal nCode As Long, ByVal wParam As Long, ByVal lParam As Long) As Long Local szTemp As WStringZ * %Max_Path, cw As CBT_CREATEWND Ptr, cst As CREATESTRUCT Ptr Function = CallNextHookEx(ByVal ghHook, ByVal nCode, ByVal wParam, ByVal lParam) If nCode < 0 Then Exit Function If nCode = %HCBT_ACTIVATE Then UnhookWindowsHookEx ghHook If nCode = %HCBT_CREATEWND Then cw = lParam ' Get pointer to CBT_CREATEWND struct so we can... TT: Nick Melnick cst = @cw.lpcs ' get a pointer to the CREATESTRUCT struct GetClassName wParam, szTemp, %Max_Path ' for each window / control as it is created ' If UCase$(szTemp) = "BUTTON" Then @cst.cx = @cst20 'prompt If UCase$(szTemp) = "BUTTON" Then @cst.x = 75 'ok and cancel If UCase$(szTemp) = "STATIC" Then @cst.y = -50 : @cst.cx = 0 : @cst.cy = 20 'prompt If UCase$(szTemp) = "EDIT" Then @cst.cx = 50 : @cst.y = @cst.y - 140 'textbox If UCase$(szTemp) = "EDIT" Then SetWindowLong wparam, %GWL_Style, GetWindowLong(wParam,%GWL_Style) If UCase$(szTemp) = "#32770" Then @cst.cx = 175 : @cst.cy = @cst.cy - 130 'dialog End If End Function
Dynamic menu caption (shows max instances setting in menu)
When there is a numerical value to be set, I like the numerical value to be visible in
the menu string. Just place a Menu Set Text line of code before the popup menu is called,
like this:
Case %WM_Notify nmtb = Cb.LParam Select Case @nmtb.hdr.Code Case %TBN_DROPDOWN Select Case @nmtb.iItem Case %IDT_Settings Menu Set Text hSettings, ByCmd %IDM_MaxInstanceCount, _ "Set Max Instance Count (" + LTrim$(Str$(MaxInstanceCount)) + ")" Call SendMessage(@nmtb.hdr.hwndFrom, %TB_GETRECT, @nmtb.iItem, VarPtr(rc)) Call MapWindowPoints(@nmtb.hdr.hwndFrom, %HWND_Desktop, ByVal VarPtr(rc), 2) Call TrackPopupMenu (hSettings, 0, rc.nLeft, rc.nBottom, 0, CbHndl, ByVal %NULL) End Select End Select
Locate MsgBox to point of use
The standard MsgBox$ does not allow you to place its window in a specified position. But
with hooking you can put it anywhere you want. You can center it in the parent dialog or
simply place it where the mouse clicked the menu option, which is what I do in gbInstanceManager.
Call the MsgBox$ with code like this:
ghHook = SetWindowsHookEx(%WH_CBT, CodePtr(SBProc), GetModuleHandle(""), GetCurrentThreadId) If MsgBox ("gbInstance already open. Open new instance?", %MB_OkCancel + _ %MB_IconInformation, "Previous Instance Check") = %IdCancel Then
and use this supporting procedure:
Function SBProc(ByVal lMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long Local pt As Point GetCursorPos pt If lMsg = %HCBT_ACTIVATE Then SetWindowPos wParam, 0, pt.x+25, pt.y+25, 0, 0, %SWP_NOSIZE Or %SWP_NOACTIVATE Or %SWP_NOZORDER UnhookWindowsHookEx ghHook End If End Function
INI File
gbInstanceManager application settings are saved in an INI file, kept in the same folder as the gbInstanceManager application.
Comments and suggestions are welcome!