WindowsNT System Manager






4.73/5 (8 votes)
Dec 1, 1999
8 min read

90604

3057
This article presents a comprehensive system control manager for NT
This article presents a comprehensive system control manager for NT.
This program was born because of a controversy among the programmers' team
I work in. One day, full of MFC resource leaks, I decide to take full control
on code. There were too many image lists, bitmaps and/or fonts handles not released
by MFC library, and NuMega BoundsChecker drives me crazy at the end of debugging.
(One day a Microsoft recruiter asked me: "what was the hardest decision you
had to take as a programmer?". I replied: "use of C, after that, C++ and now
I have go back to C". As consequence, this article does not contain any class,
any MFC improvement. As a matter of fact, no class libraries are used and there
is no .cpp file in the entire project. (I have to mention that for the readers
who may not be interested in good ol' C programming). I believe the string table
entries have quite suggestive names, so I prefer not to explain very deeply
some of the error messages, but rather let the reader to follow the idea and
supply messages with some imagination.
0. Some words about using source code and/or binaries: as 'About...'
box says, use of source code and/or binaries is permitted only with my acceptance
or by providing in Readme files, help files and/or 'About' box this mention:
"Portions developed by Zero Software Group (http://www.swac.8m.com/)".
1. Required components. In the application's initialization/checker
routine, Initialize
, there are performed some checking to see if
the program will have a chance to start. The requirements are: (1) COM initialization
successful; (2) operating system MUST be WindowsNT 4.0 or later, with Service
Pack 5 or higher (although it may be running on a lower Service Pack version,
the code is intended to be run on NT4 SP5. The code responsible for OS version
checking looks like:
/* Checks operating system version. */ { _osverInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); if(GetVersionEx(&_osverInfo)) { if(_osverInfo.dwPlatformId != VER_PLATFORM_WIN32_NT) /* Is WindowsNT? */ { LoadString(g_hInstance, IDS_ISNOTWINDOWSNT, lpszFmt, 1024); _stprintf(lpszErr, lpszFmt, _osverInfo.dwPlatformId == VER_PLATFORM_WIN32s ? _T("Windows 3.1") : (_osverInfo.dwMinorVersion ? _T("Windows '98") : _T("Windows '95"))); MessageBox(0, lpszErr, g_lpcszApplicationName, MB_OK | MB_ICONSTOP); return -2; } else if(_osverInfo.dwMajorVersion < 4) /* Is WindowsNT 4.0 or more? */ { LoadString(g_hInstance, IDS_ERR_WINDOWSNT4REQUIRED, lpszErr, 1024); MessageBox(0, lpszErr, g_lpcszApplicationName, MB_OK | MB_ICONQUESTION); return -21; } /* The service pack. Required version is 5, but user is asked if he/she wants to continue, * although it may have a lower SP version. */ if(_tcsstr(_osverInfo.szCSDVersion, _T("Service Pack"))) { TCHAR *szSPVersion, *szStopScan = _T(" \0"); nHeadLen = _tcslen(_T("Service Pack")); szSPVersion = _osverInfo.szCSDVersion + (nHeadLen + 1) * sizeof(TCHAR); lSPVersion = _tcstol(szSPVersion, &szStopScan, 10); if(lSPVersion < 5) { LoadString(g_hInstance, IDS_ERR_SP5REQUIRED, lpszFmt, 256); _stprintf(lpszErr, lpszFmt, _osverInfo.dwMajorVersion, _osverInfo.dwMinorVersion, lSPVersion); if(MessageBox(0, lpszErr, g_lpcszApplicationName, MB_YESNO | MB_ICONQUESTION) == IDNO) return -22; } } else { LoadString(g_hInstance, IDS_ERR_SP5REQUIRED, lpszFmt, 256); _stprintf(lpszErr, lpszFmt, _osverInfo.dwMajorVersion, _osverInfo.dwMinorVersion, lSPVersion); if(MessageBox(0, lpszErr, g_lpcszApplicationName, MB_YESNO | MB_ICONQUESTION) == IDNO) return -23; } } else { LoadString(g_hInstance, IDS_ERR_OSVERSION, lpszErr, 1024); MessageBox(0, lpszErr, g_lpcszApplicationName, MB_OK | MB_ICONSTOP); return -3; } }
2. The Common Controls library version which must be installed is 4.71. Checking is performed by the following code sequence:
/* Checks for Common Controls version. */ { _hCommCtl32Dll = LoadLibrary("comctl32.dll"); if(_hCommCtl32Dll) { pDllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(_hCommCtl32Dll, "DllGetVersion"); if(pDllGetVersion) { ZeroMemory(&dvi, sizeof(dvi)); dvi.cbSize = sizeof(dvi); hr = (*pDllGetVersion)(&dvi); if(SUCCEEDED(hr)) _dwVersion = MAKELONG(dvi.dwMinorVersion, dvi.dwMajorVersion); else goto _error_; } else goto _error_; goto _ok_; } else goto _error_; _error_: LoadString(g_hInstance, IDS_OLDCOMMCTL32, lpszErr, 1024); MessageBox(0, lpszErr, g_lpcszApplicationName, MB_OK | MB_ICONSTOP); return -4; _ok_: if(_hCommCtl32Dll) FreeLibrary(_hCommCtl32Dll); if(_dwVersion < _dwRequired) { LoadString(g_hInstance, IDS_OLDCOMMCTL32, lpszErr, 1024); MessageBox(0, lpszErr, g_lpcszApplicationName, MB_OK | MB_ICONSTOP); return -5; } }The comctl32.dll library is initialized by another routine,
InitializeCommonControls
, which loads list view,
progress bar, bar controls (toolbar, status bar, trackbar & tooltip) and updown control.
3. The windows (classes and windows in general). The windows' classes are registered in this order: main window, MDI child window and others.
The main window class has nothing special; but the MDI child has some extra bytes reserved in cbWndExtra
member of
WNDCLASSEX
structure used. Because a child window has an encapsulated dialog inside, these extra bytes are defined by
the size of extra bytes and the appropriate offsets inside:
#define CBWNDEXTRA ((3 + MAX_COMPUTERNAME_LENGTH + 1) * sizeof(LONG)) #define GWLAPP_HDLG (0 * sizeof(LONG)) /* encapsulated dialog window handle */ #define GWLAPP_TYPE (1 * sizeof(LONG)) /* type of items enumerated (services or device drivers) */ #define GWLAPP_STATUS (2 * sizeof(LONG)) /* status of enumerated items (active, paused, stopped) */ #define GWLAPP_MACHINE (3 * sizeof(LONG)) /* machine name - has (MAX_COMPUTERNAME_LENGTH + 1) length */
The other two window classes are used for two special windows. If one has a
role which is no more than its name shows, Legend, the other is one of
the functioning keys: the Controller window. This window receives any
control code from MDI children and forwards it to the encapsulated dialog. After
startup checks, Common Controls initialization and class registering, the CreateWindow
routine creates all windows (main window, toolbar, status bar, controller and
legend). Since the main window has a normal creation, status bar has only some
extra initializations (set parts, text and icons), and both controller and legend
are special MDI children, but normally created by WM_MDICREATE
message using a MDICREATESTRUCT
structure address passed as LPARAM.
The most interesting creation is performed in CreateToolbar
function.
The toolbar description is kept in the g_arToolbarButtons
array
of TBBUTTON
s. The Toolbar contains a combobox containing workstations,
created by CreateToolbarComboBox
function. This combo is initialized
by InitComputersCombo
, which does 3 things: (1) store the actual
window procedure in a global WNDPROC
variable (g_lpfnDefTlbCB_WndProc
),
set the combo window procedure with a new one (ToolbarCombo_WndProc
)
which does tooltip stuff and after that calls the original window procedure.
The save/replace of WNDPROC
s...
retcode SubclassToolbarCombo() { g_lpfnDefTlbCB_WndProc = (WNDPROC)GetWindowLong(g_hComboToolbarWnd, GWL_WNDPROC); SetWindowLong(g_hComboToolbarWnd, GWL_WNDPROC, (LONG)ToolbarCombo_WndProc); return 0; }
...toolbar's combo window procedure: showing tooltip...
LRESULT CALLBACK ToolbarCombo_WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: { MSG msg; HWND hWndTT; msg.lParam = lParam; msg.wParam = wParam; msg.message = uMsg; msg.hwnd = hWnd; hWndTT = (HWND)SendMessage(g_hToolBarWnd, TB_GETTOOLTIPS, 0, 0); SendMessage(hWndTT, TTM_RELAYEVENT, 0, (LPARAM)(LPMSG)&msg); break; } default: break; } return (CallWindowProc(g_lpfnDefTlbCB_WndProc, hWnd, uMsg, wParam, lParam)); }
... plus adding tooltip and setting font:
retcode AddToolbarComboTooltip() { static HWND hWndTT; static CHAR szBuf[128]; TOOLINFO lpToolInfo; hWndTT = (HWND)SendMessage(g_hToolBarWnd, TB_GETTOOLTIPS, 0, 0); if(IsWindow(hWndTT)) { lpToolInfo.cbSize = sizeof(lpToolInfo); lpToolInfo.uFlags = TTF_IDISHWND | TTF_CENTERTIP; lpToolInfo.lpszText = (LPSTR)IDM_COMPUTERCOMBO; lpToolInfo.hwnd = g_hMainWnd; lpToolInfo.uId = (UINT)g_hComboToolbarWnd; lpToolInfo.hinst = g_hInstance; SendMessage(hWndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&lpToolInfo); } return 0; } retcode SetToolbarComboFont() { return SendMessage(g_hComboToolbarWnd, WM_SETFONT, (WPARAM)(HFONT)SendMessage(g_hToolBarWnd, WM_GETFONT, 0, 0), MAKELPARAM(1, 0)); }
The enumeration of items which may fill the toolbar's combo, the workstations, is performed by the FillComputersCombo
function.
Nothing special, it is one of the hundreds of IShellFolder
enumeration samples you have seen already. Maybe the lpVtbl
could appear
weird for the newcomers; it's the interface's virtual table pointer. When using the "C" call, remember to pass also the "this" pointer, as in
_cleanup_: if(lpe) lpe->lpVtbl->Release(lpe); // more release to come
4. The actions and the thread routines. You can manage almost all via toolbar, menu or accelerators. Open shows a MDI child with the dialog inside. The dialog's list controls will contain the appropriate items, depending on the buttons 5-10: if 5-6 (


MDIChild_OnCreate
will be
allocated a LPSERVICEFILTER
variable, containing information's about
enumeration (item type and state, two event handles - 'Cancel' and 'Close', necessary
window handles, thread ID and computer name):
/* service filter - used to display data verifying certain conditions */ typedef struct _tagSERVICEFILTER { BOOL fService; /* service */ BOOL fDevice; /* device driver */ BOOL fRunning; /* running service/device */ BOOL fPaused; /* paused service/device */ BOOL fStopped; /* stopped service/device */ HWND hwndLV; /* list view handle */ HWND hwndDlg; /* encapsulated dialog handle */ HWND hwndProgr; /* progress bar handle */ HANDLE hCancelEvent; /* "cancel" event handle */ HANDLE hCloseEvent; /* "close window" event handle */ unsigned uThreadId; /* associated thread ID */ TCHAR lpszComputerName[_MAX_PATH + 1]; /* computer name */ } SERVICEFILTER, *LPSERVICEFILTER;The dialog is created using
CreateDialogParam
, passing the allocated LPSERVICEFILTER
as lParam
.
In dialog initialization (WM_INITDIALOG
interception), will be called MDIChildDlg_OnInitList
. Here is the place where we have necessary window
handles and we can launch the enumeration thread routine, FillServicesList
. Since the resize, progress bar advance etc. are common stuff, the enumeration can be the only 'uncommon' thing:
schSCM = OpenSCManager(lpszUNCName, 0, SC_MANAGER_ALL_ACCESS); if(schSCM) { dwResumeHandle = 0; #pragma warning(disable:4127) do { _retry_: bEnumRetVal = EnumServicesStatus ( schSCM, dwType, dwServiceState, lpSvc, dwBufferSize, &dwBytesNeeded, &dwServicesReturned, &dwResumeHandle ); if(!bEnumRetVal) { g_dwLastError = GetLastError(); if(g_dwLastError == ERROR_MORE_DATA) { #pragma warning(disable:4127) if(lpSvc != 0) SafeDeletePointer(lpSvc, dwBytesNeeded); SafeAllocatePointer(lpSvc, LPENUM_SERVICE_STATUS, GPTR, dwBytesNeeded); if(lpSvc) { dwBufferSize = dwBytesNeeded; lpSvc->lpDisplayName = lpszDisplayName; lpSvc->lpServiceName = lpszServiceName; goto _retry_; } else goto _cleanup_; } else if(g_dwLastError == ERROR_NO_MORE_ITEMS) break; else goto _cleanup_; } else { // insert all entries in list register DWORD x = 0; for(x = 0; x < dwServicesReturned; x++) { TCHAR *lpszStatusString = 0, *lpszStartupString = 0; short sStatusImage = IMAGE_UNKNOWN; short sStartupImage = IMAGE_UNKNOWN; DWORD dwStartType = 0; { schService = OpenService(schSCM, lpSvc[x].lpServiceName, SERVICE_ALL_ACCESS); if(schService) { QUERY_SERVICE_CONFIG *pqsc; DWORD _dwFalseSize = 0; DWORD _dwBytesNeeded = 0; char lpszBPN[_MAX_PATH + 1]; char lpszDep[_MAX_PATH + 1]; char lpszDN[_MAX_PATH + 1]; char lpszLOG[_MAX_PATH + 1]; char lpszSSN[_MAX_PATH + 1]; _dwFalseSize = sizeof(QUERY_SERVICE_CONFIG); _dwFalseSize += 5 * (_MAX_PATH + 1); SafeAllocatePointer(pqsc, LPQUERY_SERVICE_CONFIG, GPTR, _dwFalseSize); pqsc->lpBinaryPathName = lpszBPN; pqsc->lpDependencies = lpszDep; pqsc->lpDisplayName = lpszDN; pqsc->lpLoadOrderGroup = lpszLOG; pqsc->lpServiceStartName = lpszSSN; QueryServiceConfig(schService, pqsc, _dwFalseSize, &_dwBytesNeeded); dwStartType = pqsc->dwStartType; #pragma warning(disable:4127) SafeDeletePointer(pqsc, _dwFalseSize); CloseServiceHandle(schService); schService = 0; } } if(GetStatusString(lpSvc[x].ServiceStatus.dwCurrentState, (TCHAR **)&lpszStatusString) && GetAssociatedStatusImage(lpSvc[x].ServiceStatus.dwCurrentState, &sStatusImage) && GetAssociatedStartupImage(dwStartType, &sStartupImage) && GetStartupString(dwStartType, (TCHAR **)&lpszStartupString)) { if(psf->fPaused && !(lpSvc[x].ServiceStatus.dwCurrentState == SERVICE_PAUSED)) continue; if(psf->fRunning && !(lpSvc[x].ServiceStatus.dwCurrentState == SERVICE_RUNNING)) continue; { DWORD dwId = 0; // advance progress bar IsWindow(hwndProgr) && SendMessage(hwndProgr, PBM_STEPIT, 0, 0); InsertRowInList(hwndLV, 9, &dwId, sStatusImage, lpSvc[x].lpServiceName, -1, lpSvc[x].lpDisplayName, sStatusImage, lpszStatusString, sStartupImage, lpszStartupString); #pragma warning(disable:4127) SafeDeletePointer(lpszStatusString, _tcslen(lpszStatusString)); SafeDeletePointer(lpszStartupString, _tcslen(lpszStartupString)); } } } } if(dwResumeHandle == 0) // enumeration ends, succeeded break; } while(TRUE); _cleanup_: #pragma warning(disable:4127) if(lpSvc != 0) SafeDeletePointer(lpSvc, dwBytesNeeded); CloseServiceHandle(schSCM); }The 'Cancel' and 'Close' events are not intercepted, but in the Event Log sample you will see how to 'WaitForSingleObject' them. Here the enumeration cannot produce so many items (as Event log does).
The following five buttons, 11-15 (

OnControl
, with the appropriate control code.
A special mention for start: because control code for stop is 1, the code 0 (SERVICE_CONTROL_STOP - 1) is used to signal a start request. Although I doubt it, this may be wrong in a future version of header file <winsvc.h>.
Using the same technique, aLPCONTROLSERVICE
is allocated as passed as LPARAM to controller window, via
user-defined message WM_USER_CTLSERVICE. The controller window procedure send another user-defined message to encapsulated dialog, WM_USER_REQUESTCTLSERVICE,
to show in controller dialog what request was captured. After this, the OnControlService
thread routine is called.
typedef struct _tagCONTROLSERVICE { DWORD dwControlCode; /* control code */ BOOL fService; /* TRUE if service, FALSE if device driver */ DWORD dwErrorCode; /* DWORD error code */ HWND hwndLV; /* the list view window which launch the request */ UINT uListIdx; /* index of item in list view */ SERVICE_STATUS ServiceStatus; /* item status, if success change item icon */ TCHAR lpszMachineName[MAX_COMPUTERNAME_LENGTH + 1]; /* machine name */ TCHAR lpszServiceName[_MAX_PATH + 1]; /* service name (real, not diplay name) */ } CONTROLSERVICE, *LPCONTROLSERVICE;
unsigned int __stdcall OnControlService(LPVOID lpParam) { unsigned int uRetVal = 0; TCHAR lpszMachineName[_MAX_PATH + 1], lpszServiceName[_MAX_PATH + 1]; DWORD dwCtlCode; BOOL fService, bSuccess = FALSE; SC_HANDLE schSCM = 0, schService = 0; SERVICE_STATUS _ServiceStatus; LPCONTROLSERVICE pCtlService; // passed parameter pCtlService = (LPCONTROLSERVICE)lpParam; // obtain description of requested operation _tcscpy(lpszMachineName, pCtlService->lpszMachineName); _tcscpy(lpszServiceName, pCtlService->lpszServiceName); dwCtlCode = pCtlService->dwControlCode; fService = pCtlService->fService; // the job // open SCM schSCM = OpenSCManager((LPCTSTR)lpszMachineName, NULL, GENERIC_READ | GENERIC_EXECUTE); if(schSCM) { // open service schService = OpenService(schSCM, (LPCTSTR)lpszServiceName, GENERIC_READ | GENERIC_EXECUTE); if(schService) { // control item if(dwCtlCode == SERVICE_CONTROL_STOP - 1) // meaning start { bSuccess = StartService(schService, 0, NULL); pCtlService->ServiceStatus.dwCurrentState = SERVICE_RUNNING; // we have to enforce this to signal } else { bSuccess = ControlService(schService, dwCtlCode, &_ServiceStatus); pCtlService->ServiceStatus = _ServiceStatus; } // close service handle CloseServiceHandle(schService); } // close SCM handle CloseServiceHandle(schSCM); } if(!bSuccess) pCtlService->dwErrorCode = g_dwLastError = GetLastError(); else pCtlService->dwErrorCode = 0; // notify controller window by operation status SendMessage(g_hControllerWnd, WM_USER_NOTIFYCTLSTATUS, (WPARAM)0, (LPARAM)pCtlService); // destroy allocated memory - we don't need this pointer anymore #pragma warning(disable:4127) SafeDeletePointer(pCtlService, sizeof(CONTROLSERVICE)); return uRetVal; }Before returning, another user-defined message, WM_USER_NOTIFYCTLSTATUS, ensures reporting back to controller window using the same parameter. As you probably noticed already, the caller routine allocates and fills (alone or using other routines) the passed LPVOID parameter; the called thread routine is responsable for freeing memory. Two macros, which appear to behave normally until this moment, helps me in memory management:
#define SafeAllocatePointer(pointer,type,flags,size) (pointer)=(type)(GlobalAlloc((flags),(size))) #define SafeDeletePointer(block,size) \ {\ if((size)>=0)\ {\ if((block) && !(IsBadReadPtr((block),(size)))) \ {\ GlobalFree((block));\ (block)=0;\ }\ }\ }
5. The managers are governed by the buttons 16-18 (). New service, modify and delete: these are the 3 actions
possible. Although New and Open are valid only for local machine, mapping network drive is intended to support also service management on remote machine, after
the appropriate path translation. All the work on all the controls (for new and modify) is based on a good reading of MSDN. The dialog calls only one function,
depending if is a new service or an existing one:
CreateService
or ChangeServiceConfig
;
for service deletion, the API routine DeleteService
ensures the job.
6. The helpers are managed by the buttons 19-24 (). The first two buttons have simple jobs to do; the first launches Control panel,
and the second the Control Panel' Services applet. Maybe this could be interesting? And something else: don't ask me how I get the dialog ID CPLAPPLET_SRVMGR_SERVICESDLGID
(1). This could have another ID in Windows2000, for example.
LRESULT* OnCPLServices(HWND hWnd, LRESULT *plResult) { TCHAR lpszCPLApplet_SrvMgr[_MAX_PATH + 1]; _UNREFERENCED_PARAMETER_(hWnd); *plResult = 1; // assume fail /* Loads services' applet name. */ if(LoadString(g_hInstance, IDS_CPLAPPLET_SRVMGR, lpszCPLApplet_SrvMgr, _MAX_PATH + 1)) { HMODULE hCPLApplet_SrvMgr = 0; /* Loads srvmgr.cpl. */ hCPLApplet_SrvMgr = LoadLibrary(lpszCPLApplet_SrvMgr); if(hCPLApplet_SrvMgr) { /* Get CPLApplet exported routine, and then call a dialog using CPL_INQUIRE message. */ g_lpfnCPlApplet = (APPLET_PROC)GetProcAddress(hCPLApplet_SrvMgr, _T("CPlApplet")); if(g_lpfnCPlApplet) { if((*g_lpfnCPlApplet)(0, CPL_INQUIRE, CPLAPPLET_SRVMGR_SERVICESDLGID, (LPARAM)&g_cpli) == 0) { if((*g_lpfnCPlApplet)(0, CPL_DBLCLK, CPLAPPLET_SRVMGR_SERVICESDLGID, 0) == 0) // 0 for OK { *plResult = 0; // terminate with success g_lpfnCPlApplet = 0; // Signal for OK closing application. OnClose will close applet instead of program. } } } FreeLibrary(hCPLApplet_SrvMgr); } } if(*plResult != 0) ReportLastError(0, 0, TRUE, 0); *plResult = 0; return plResult; }The next two have the role of network disk mapping. In fact, they are simple calls to OnNetworkDriveOp handler, which is using the mpr.dll library and look for
WNetConnectionDialog
or WNetDisconnectDialog
. These buttons will be used, as I already said, at remote
service installation.
And finally, of course, the delight of remote shutdown. When I posted the previous dialog-based MFC version, suddenly many people became VERY interested...Now in a API version, calling the same InitiateSystemShutdown library function. The "countdown" dialog box is waiting you to hit 'Cancel' using the
ShutdownWorkstation
thread routine. If you don't want this, that's
it. Wait for the next logon. The structure pointer used this time to pass in LPVOID's
thread routine is SHUTDOWNSTRUCT. Again, the caller allocates
memory and the called frees it.
typedef struct _tagSHUTDOWNSTRUCT { BOOL fLocalComputer; /* local computer */ BOOL fForceApplicationClose; /* force application's closing */ BOOL fReboot; /* reboot after shutdown */ UINT nTimeout; /* timeout in seconds */ HANDLE hCancelEvent; /* 'Cancel' event (abort shutdown if set) */ unsigned int uThreadId; /* 'Wait for Cancel' thread id */ HWND hwndImage; /* flip image */ HWND hwndStatus; /* status bar */ HWND hwndProgress; /* progress bar (left seconds) */ TCHAR lpszMachineName[MAX_COMPUTERNAME_LENGTH + 1]; /* machine name */ TCHAR lpszMessage[1024]; /* message prompted to user */ } SHUTDOWNSTRUCT, *LPSHUTDOWNSTRUCT;
unsigned int __stdcall ShutdownWorkstation(LPVOID lpParam) { LPSHUTDOWNSTRUCT lpSS = 0; TCHAR lpszFmt[256], lpszText[1024]; unsigned int uRetVal= 0; BOOL bFlipImage = TRUE; UINT i = 0; DWORD dw = WAIT_OBJECT_0; HWND hwndImage = 0; HWND hwndStatus = 0; HWND hwndProgress = 0; DWORD dwTickStart = 0; DWORD dwTickEnd = 0; lpSS = (LPSHUTDOWNSTRUCT)lpParam; hwndImage = lpSS->hwndImage; hwndStatus = lpSS->hwndStatus; hwndProgress = lpSS->hwndProgress; dwTickStart = GetTickCount(); LoadString(g_hInstance, IDS_SHUTDOWNPROGRESS, lpszFmt, 256); _stprintf(lpszText, lpszFmt, lpSS->lpszMachineName, lpSS->nTimeout - i); SetWindowText(hwndStatus, lpszText); SendMessage(hwndProgress, PBM_SETRANGE, 0, MAKELPARAM(0, (short)(lpSS->nTimeout))); SendMessage(hwndProgress, PBM_SETPOS, 0, 0); while(TRUE) { dw = WaitForSingleObject(lpSS->hCancelEvent, 0); if((lpSS->nTimeout <= i) || (dw == WAIT_OBJECT_0)) break; dwTickEnd = GetTickCount(); if(dwTickEnd - dwTickStart >= 1000) { i++; SendMessage(hwndProgress, PBM_SETPOS, (WPARAM)i, 0); _stprintf(lpszText, lpszFmt, lpSS->lpszMachineName, lpSS->nTimeout - i); SetWindowText(hwndStatus, lpszText); dwTickStart = dwTickEnd; bFlipImage = !bFlipImage; if(bFlipImage) ShowWindow(hwndImage, SW_SHOW); else ShowWindow(hwndImage, SW_HIDE); } } if(dw == WAIT_TIMEOUT) { PostMessage(GetParent(hwndStatus), WM_USER_DLGDESTROY, 0, 0); if(lpSS->fLocalComputer) { UINT uFlags = (lpSS->fReboot ? EWX_REBOOT : 0) | // reboot after shutdown (lpSS->fForceApplicationClose ? EWX_FORCE : 0); // force closing for application(s) ExitWindowsEx(uFlags, 0L); } } else { PostMessage(GetParent(hwndStatus), WM_USER_DLGDESTROY, 1, 0); AbortSystemShutdown((LPSTR)lpSS->lpszMachineName); } // deallocate event CloseHandle(lpSS->hCancelEvent); // destroy allocated memory - we don't need this pointer anymore #pragma warning(disable:4127) SafeDeletePointer(lpSS, sizeof(SHUTDOWNSTRUCT)); lpSS = 0; return uRetVal; }
7. The controls. Maybe list control implementation file, ListViewX.c, contains some useful things:
- a simple column count:
short ListView_GetColumnCount(HWND hwndLV) { short sCol = 0; LVCOLUMN lvCol; lvCol.mask = LVCF_WIDTH; while(ListView_GetColumn(hwndLV, sCol++, &lvCol)) ; return --sCol; }
- sorting list, using another custom structure (
DLGSORTDATA
) ...
/* sort data - structure used to pass when a list's header is clicked */ typedef struct _tagDLGSORTDATA { HWND hDlg; /* dialog handle */ int nSortOrder; /* sorting order */ int nSortType; /* sorting type */ int nColIdx; /* column index */ } DLGSORTDATA, *LPDLGSORTDATA;and the description array of list columns,
g_pcListColumns
of type
/* list view column */ typedef struct _tagLISTVIEWCOLUMN { UINT uCXCol; /* index */ int nSortType; /* sorting type (STRING = 0, NUMERIC, DATE, DATETIME) */ int nSortOrder; /* sorting order (ASCENDING = -1, NONE, DESCENDING) */ TCHAR lpszName[32]; /* column name */ } LISTVIEWCOLUMN, *LPLISTVIEWCOLUMN;to sort the list:
int ListView_OnSort(HWND hDlg, HWND hwndLV, int iColumn, PFNLVCOMPARE lpfnCompare) { int nColCount; nColCount = ListView_GetColumnCount(hwndLV); if(iColumn < nColCount) { DLGSORTDATA ds; if(g_pcListColumns[iColumn].nSortOrder == ASCENDING) g_pcListColumns[iColumn].nSortOrder = DESCENDING; else if(g_pcListColumns[iColumn].nSortOrder == DESCENDING) g_pcListColumns[iColumn].nSortOrder = NONE; else g_pcListColumns[iColumn].nSortOrder = ASCENDING; ds.hDlg = hDlg; ds.nSortOrder = g_pcListColumns[iColumn].nSortOrder; ds.nSortType = g_pcListColumns[iColumn].nSortType; ds.nColIdx = iColumn; return (int)ListView_SortItems(hwndLV, lpfnCompare, (LPARAM)(&ds)); } else return 0; }Also, the variable-arguments routine
InsertRowInList
helps a lot on the insert operation in list. Although custom, this
routine could be easily adapted to serve your own purpose.
BOOL InsertRowInList(HWND hwndLV, int nInsertCount, ...) { int nCount, nItem; va_list marker; DWORD dwId; register int i; LVITEM lvItem; if(!IsWindow(hwndLV)) return FALSE; nCount = ListView_GetItemCount(hwndLV); lvItem.mask = LVIF_TEXT | LVIF_PARAM; lvItem.pszText = " "; lvItem.cchTextMax = strlen(" "); lvItem.iItem = nCount; lvItem.iImage = 0; lvItem.iSubItem = 0; lvItem.state = 0; lvItem.stateMask = 0; lvItem.iIndent = 0; lvItem.lParam = nCount; nItem = ListView_InsertItem(hwndLV, &lvItem); va_start(marker, nInsertCount); dwId = *((DWORD *)(va_arg(marker, DWORD*))); lvItem.mask = LVIF_PARAM; lvItem.iItem = nCount; lvItem.iSubItem = 0; lvItem.lParam = (LPARAM)dwId; ListView_SetItem(hwndLV, &lvItem); for(i = 0; i < nInsertCount - 1; i += 2) { char *szItemText; short sImage; sImage = va_arg(marker, short); szItemText = va_arg(marker, char*); lvItem.mask = LVIF_TEXT | (sImage == -1 ? 0 : LVIF_IMAGE); lvItem.iItem = nCount; lvItem.iSubItem = i / 2; lvItem.iImage = sImage; lvItem.pszText = szItemText; lvItem.cchTextMax = strlen(szItemText); ListView_SetItem(hwndLV, &lvItem); } va_end(marker); return TRUE; }And finally, a routine to ensure full visibility of a list column (both header and text inside). It works, for the moment, only if the list has report style. But I am waiting for serious improvement.
BOOL ListView_EnsureColumnVisible(HWND hwndLV, short sColIndex) { int iCount, nLVCount = ListView_GetItemCount(hwndLV), nMaxWidth = 0, nMaxHeaderWidth = 0; HDC hLVDC; // ensure some conditions // is a window? if(!IsWindow(hwndLV)) return FALSE; // is a listview? { TCHAR lpszLVClassName[_MAX_PATH + 1]; if(!GetClassName(hwndLV, lpszLVClassName, _MAX_PATH + 1) || (_tcsicmp(lpszLVClassName, WC_LISTVIEW) != 0)) return FALSE; } // is in report style? if(!(GetWindowLong(hwndLV, GWL_STYLE) & LVS_REPORT)) return FALSE; // column out of range if(sColIndex > ListView_GetColumnCount(hwndLV)) // it is 0-based return FALSE; nMaxWidth = ListView_GetColumnWidth(hwndLV, sColIndex); hLVDC = GetDC(hwndLV); if(hLVDC) { // get image width, if column has subitem images int nIconSize = 0; { HIMAGELIST himgList = ListView_GetImageList(hwndLV, LVSIL_SMALL); if(himgList) { int cx, cy; if(ImageList_GetIconSize(himgList, &cx, &cy)) { nIconSize = cx; } } } for(iCount = 0; iCount < nLVCount; iCount++) { LVITEM lvItem; TCHAR lpszBuffer[_MAX_PATH + 1]; SIZE sizeLVText; // ensure also extra space if column have and image inside lvItem.mask = LVIF_TEXT | LVIF_IMAGE; lvItem.pszText = lpszBuffer; lvItem.cchTextMax = _MAX_PATH + 1; lvItem.iItem = iCount; lvItem.iSubItem = sColIndex; lvItem.iImage = -1; if(ListView_GetItem(hwndLV, &lvItem)) { if(GetTextExtentPoint32(hLVDC, lpszBuffer, _tcslen(lpszBuffer), &sizeLVText)) { if(lvItem.iImage >= 0) // also has image { if(nMaxWidth < sizeLVText.cx + nIconSize) nMaxWidth = sizeLVText.cx + nIconSize; } else { if(nMaxWidth < sizeLVText.cx) nMaxWidth = sizeLVText.cx; } } } } } ReleaseDC(hwndLV, hLVDC); // last test: need header greater size? { HWND hwndHD; hwndHD = ListView_GetHeader(hwndLV); if(hwndHD) { HDITEM hdItem; TCHAR lpszHeaderText[_MAX_PATH + 1]; hdItem.mask = HDI_BITMAP | HDI_TEXT; hdItem.pszText = lpszHeaderText; hdItem.cchTextMax = _MAX_PATH + 1; hdItem.hbm = 0; if(Header_GetItem(hwndHD, sColIndex, &hdItem)) { SIZE sizeHDText; HDC hHDDC; hHDDC = GetDC(hwndHD); if(GetTextExtentPoint32(hHDDC, lpszHeaderText, _tcslen(lpszHeaderText), &sizeHDText)) { // assume text only nMaxHeaderWidth = sizeHDText.cx; if(hdItem.hbm) // has bitmap { SIZE sizeBmp; if(GetBitmapDimensionEx(hdItem.hbm, &sizeBmp)) nMaxHeaderWidth += sizeBmp.cx; } } ReleaseDC(hwndHD, hHDDC); } } } if(nMaxWidth < nMaxHeaderWidth) nMaxWidth = nMaxHeaderWidth; // now, since we established the maximum string size inside column, ensure this new column size return ListView_SetColumnWidth(hwndLV, sColIndex, nMaxWidth); }Also the 'About' Box contains a primitive 'Hyperlink' behaviour, but in fact is some GDI work and an ordinary static control changing the bitmap inside. The scrolling is ensured by the ScrollWindow routine. The 'singing' part, present also in the shutdown options dialog, has some MCI dealings. Common, but let's see it:
#pragma warning(disable:4702) BOOL* AboutBox_MCIPlaySound(HWND hWnd, WPARAM wParam, LPARAM lParam, BOOL *pbResult) { TCHAR lpszVFWLibrary[_MAX_PATH + 1]; TCHAR lpszMCIWndRegisterClass[_MAX_PATH + 1]; TCHAR lpszWAVFile[_MAX_PATH + 1]; _UNREFERENCED_PARAMETER_(hWnd); _UNREFERENCED_PARAMETER_(wParam); _UNREFERENCED_PARAMETER_(lParam); // Load strings for library name, routine and sound file. If OK, try to "sing" utopia.wav. if(LoadString(g_hInstance, IDS_VFW32DLL, lpszVFWLibrary, _MAX_PATH + 1) && LoadString(g_hInstance, IDS_MCIWNDREGISTERCLASS, lpszMCIWndRegisterClass, _MAX_PATH + 1) && LoadString(g_hInstance, IDS_ABOUTBOXWAVFILE, lpszWAVFile, _MAX_PATH + 1)) { g_hVFW32 = LoadLibrary(lpszVFWLibrary); if(g_hVFW32) // we have audio support { // Find MCIWndRegisterClass exported routine. lpfnMCIWndRegisterClass = (LPFNMCIWNDREGISTERCLASS)GetProcAddress(g_hVFW32, lpszMCIWndRegisterClass); if(lpfnMCIWndRegisterClass) // ok, create MCI window, if possible { MDICREATESTRUCT mdics; mdics.szClass = MCIWND_WINDOW_CLASS; mdics.szTitle = _T("__ZS_InvisibleMCIWindow__"); mdics.hOwner = g_hInstance; mdics.x = CW_USEDEFAULT; mdics.y = 0; mdics.cx = CW_USEDEFAULT; mdics.cy = 0; mdics.style = WS_CLIPCHILDREN | WS_CLIPSIBLINGS | MCIWNDF_SHOWALL; mdics.lParam = 0; if((*lpfnMCIWndRegisterClass)()) // registering: success { g_hwndMdi = CreateWindow ( MCIWND_WINDOW_CLASS, _T("InvisibleMCIWindow"), WS_CLIPCHILDREN | WS_CLIPSIBLINGS | MCIWNDF_SHOWALL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, 0, g_hInstance, (LPVOID)(LPSTR)(LPCTSTR)lpszWAVFile ); if(g_hwndMdi) // MCI window created: hide and play { ShowWindow(g_hwndMdi, SW_HIDE); UpdateWindow(g_hwndMdi); SendMessage(g_hwndMdi, MCI_PLAY, 0, 0); } } } } } *pbResult = TRUE; return pbResult; }