Visual C++是一種物件導向的可視化編程工具,它提供的AppWizard能自動產生應用程式的標準架構,大大減輕了編程的工作量。本文主要介紹如下的編程技巧:修改主視窗風格、建立不規則形狀視窗、用按一下滑鼠視窗標題條以外地區移動視窗、使用操作功能表、使應用程式只能運行一個執行個體、使應用程式顯示為任務條通知區中的表徵圖和顯示旋轉文本等。
1. 修改主視窗風格
AppWizard產生的應用程式架構的主視窗具有預設的視窗風格,比如在視窗標題條中自動添加文檔名、視窗是疊加型的、可改變視窗大小等。要修改視窗的預設風格,需要重載CWnd::PreCreateWindow(CREATESTRUCT& cs)函數,並在其中修改CREATESTRUCT型參數cs。
CWnd::PreCreateWindow 函數先於視窗建立函數執行。如果該函數被重載,則視窗建立函數將使用CWnd::PreCreateWindow 函數返回的CREATESTRUCT cs參數所定義的視窗風格來建立視窗;否則使用預定義的視窗風格。
CREATESTRUCT結構定義了建立函數建立視窗所用的初始參數,其定義如下:
typedef struct tagCREATESTRUCT {
LPVOID lpCreateParams; // 建立視窗的基本參數
HANDLE hInstance; // 擁有將建立的視窗的模組執行個體控制代碼
HMENU hMenu; // 新視窗的菜單控制代碼
HWND hwndParent; // 新視窗的父視窗控制代碼
int cy; // 新視窗的高度
int cx; // 新視窗的寬度
int y; // 新視窗的左上方Y座標
int x; // 新視窗的左上方X座標
LONG style; // 新視窗的風格
LPCSTR lpszName; // 新視窗的名稱
LPCSTR lpszClass; // 新視窗的視窗類別名
DWORD dwExStyle; // 新視窗的擴充參數
} CREATESTRUCT;
CREATESTRUCT結構的style域定義了視窗的風格。比如,預設的MDI主視窗的風格中就包括FWS_ADDTOTITLE(在標題條中顯示當前的工作文檔名)、FWS_PREFIXTITLE(把文檔名放在程式標題的前面)、WS_THICKFRAME(視窗具有可縮放的邊框)等風格。由於多種風格參數由邏輯或(“|”)組合在一起的,因此添加某種風格,就只需用“|”把對應的參數加到CREATESTRUCT結構的style域中;刪除已有的風格,則需用“&”串連CREATESTRUCT結構的style域與該風格的邏輯非值。
CREATESTRUCT結構的x、y、cx、cy域分別定義了視窗的初始位置和大小,因此,在CWnd::PreCreateWindow 函數中給它們賦值,將能定義視窗的初始顯示位置和大小。
下例中的代碼將主框視窗的大小將固定為1/4螢幕,標題條中僅顯示視窗名,不顯示文檔名。
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
// 修改主窗風格
cs.style &= ~FWS_ADDTOTITLE; //去除標題條中的文檔名
cs.style &= ~WS_THICKFRAME; //去除可改變大小的邊框
cs.style |= WS_DLGFRAME; //增加不能改變大小的邊框
// 確定主窗的大小和初始位置
int cxScreen = ::GetSystemMetrics(SM_CXSCREEN);//獲得螢幕寬
int cyScreen = ::GetSystemMetrics(SM_CYSCREEN); //獲得螢幕高
cs.x = 0; // 主窗位於左上方
cs.y = 0;
cs.cx = cxScreen/2; // 主窗寬為1/2螢幕寬
cs.cy = cxScreen/2; // 主窗高為1/2螢幕高
return CMDIFrameWnd::PreCreateWindow(cs);
}
2. 建立不規則形狀視窗
標準的Windows視窗是矩形的,但在有些時候我們需要非矩形的視窗,比如圓形的、甚至是不規則的。藉助CWnd類的SetWindowRgn函數可以建立不規則形狀視窗。
CWnd::SetWindowRgn的函數原型如下:
int SetWindowRgn( HRGN hRgn, // 視窗地區控制代碼
BOOL bRedraw ); // 是否重畫視窗
CRgn類封裝了關於地區的資料和操作。通過(HRGN)強制操作可以從CRgn類中取得其HRGN值。
CRgn提供了CreateRectRgn、CreateEllipticRgn和CreatePolygonRgn成員函數,分別用以建立矩形、(橢)圓形和多邊形地區。
建立非矩形視窗的方法如下:首先,在視窗類別中定義地區類成員資料(如CRgn m_rgnWnd);其次,在視窗的OnCreate函數或對話方塊的OnInitDialog函數中調用CRgn類的CreateRectRgn、CreateEllipticRgn或CreatePolygonRgn函數建立所需的地區,並調用SetWindowRgn函數。
下例將產生一個橢圓視窗。
1. 在Developer Studio中選取File菜單中的New命令,在出現的New對話方塊中選擇建立MFC AppWizard(exe)架構應用程式,並輸入項目名為EllipseWnd。設定應用程式類型為基於對話方塊(Dialog based),其它選項按預設值建立項目源檔案。
2. 使用資源編輯器從主對話方塊(ID為IDD_ELLIPSEWND_DIALOG)刪除其中的所有控制,並從其屬性對話方塊(Dialog Properties)中設定其風格為Popup、無標題條和邊框。
3. 在EllipseWndDlg.h源檔案中給主對話方塊類CEllipseWndDlg增加一個CRgn類保護型資料成員m_rgnWnd,它將定義視窗的地區。
4. 在EllipseWndDlg.cpp源檔案中修改主對話方塊類CEllipseWndDlg的OnInitDialog()函數,增加m_rgnWnd的建立,並將其定義為視窗地區。粗體語句為新增部分。
BOOL CEllipseWndDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX,
strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// 設定視窗標題為“橢圓視窗”,雖然對話方塊沒有標題條,
// 但在任務條的按鈕中仍需要標題
SetWindowText(_T("橢圓視窗"));
// 取得螢幕寬、高
int cxScreen = ::GetSystemMetrics(SM_CXSCREEN);
int cyScreen = ::GetSystemMetrics(SM_CYSCREEN);
// 設定橢圓X、Y方向的半徑
int nEllipseWidth = cxScreen/8;
int nEllipseHeight = cyScreen/8;
// 將視窗大小設為寬nEllipseWidth,高nEllipseHeight
// 並移至左上方
MoveWindow(0, 0, nEllipseWidth, nEllipseHeight);
// 建立橢圓地區m_rgnWnd
m_rgnWnd.CreateEllipticRgn(0, 0, nEllipseWidth, nEllipseHeight);
// 將m_rgnWnd設定為視窗地區
SetWindowRgn((HRGN)m_rgnWnd, TRUE);
return TRUE; // return TRUE unless you set the focus to a control
}
3. 用按一下滑鼠視窗標題條以外地區移動視窗
移動標準視窗是通過用按一下滑鼠視窗標題條來實現的,但對於沒有標題條的視窗,就需要用按一下滑鼠視窗標題條以外地區來移動視窗。有兩種方法可以達到這一目標。
方法一:當視窗確定滑鼠位置時,Windows向視窗發送WM_NCHITTEST訊息,可以處理該訊息,使得只要滑鼠在視窗內,Windows便認為滑鼠在標題條上。這需要重載CWnd類處理WM_NCHITTEST訊息的OnNcHitTest函數,在函數中調用父類的該函數,如果返回HTCLIENT,說明滑鼠在視窗客戶區內,使重載函數返回HTCAPTION,使Windows誤認為滑鼠處於標題條上。
下例是使用該方法的實際代碼:
UINT CEllipseWndDlg::OnNcHitTest(CPoint point)
{
// 取得滑鼠所在的視窗地區
UINT nHitTest = CDialog::OnNcHitTest(point);
// 如果滑鼠在視窗客戶區,則返回標題條代號給Windows
// 使Windows按滑鼠在標題條上類進行處理,即可單擊移動視窗
return (nHitTest==HTCLIENT) ? HTCAPTION : nHitTest;
}
方法二:當使用者在視窗客戶區按下滑鼠左鍵時,使Windows認為滑鼠是在標題條上,即在處理WM_LBUTTONDOWN訊息的處理函數OnLButtonDown中發送一個wParam參數為HTCAPTION,lParam為當前座標的WM_NCLBUTTONDOWN訊息。
下面是使用該方法的實際代碼:
void CEllipseWndDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
// 調用父類處理函數完成基本操作
CDialog::OnLButtonDown(nFlags, point);
// 發送WM_NCLBUTTONDOWN訊息
// 使Windows認為滑鼠在標題條上
PostMessage(WM_NCLBUTTONDOWN,
HTCAPTION,
MAKELPARAM(point.x, point.y));
}
4. 使用操作功能表
Windows 95應用程式支援單擊滑鼠右鍵彈出操作功能表的功能,這可通過處理WM_CONTEXTMENU訊息來實現。
當在視窗內單擊滑鼠右鍵時,視窗將接收到WM_CONTEXTMENU訊息,在該訊息的處理函數內裝載操作功能表,並調用CMenu::TrackPopupMenu函數便可顯示操作功能表。CMenu::TrackPopupMenu函數的原型如下:
BOOL TrackPopupMenu( UINT nFlags, // 顯示和選取方式標誌
int x, int y, // 顯示菜單的左上方座標
CWnd* pWnd, // 接收菜單操作的視窗對象
LPCRECT lpRect = NULL ); // 敏感地區
為了使用操作功能表,首先應在資源編輯器中編製好操作功能表,假設操作功能表名為IDR_MENU_CONTEXT;其次,用ClassWizard給視窗增加處理訊息WM_CONTEXTMENU的函數OnContextMenu,以及各功能表命令的處理函數;然後編寫相應的代碼。
下面的是OnContextMenu函數的代碼執行個體:
void CEllipseWndDlg::OnContextMenu(CWnd* pWnd, CPoint point)
{
CMenu menu;
// 裝入菜單
menu.LoadMenu(IDR_MENU_CONTEXT);
// 顯示菜單
menu.GetSubMenu(0)->TrackPopupMenu(
TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RIGHTBUTTON,
point.x, point.y, this);
}
5. 使應用程式只能運行一個執行個體
Windows是多進程作業系統,架構產生的應用程式可以多次運行,形成多個運行執行個體。但在有些情況下為保證應用程式的安全運行,要求程式只能運行一個執行個體,比如程式要使用只能被一個進程單獨使用的特殊硬體(例如數據機)時,必須限制程式只運行一個執行個體。
這裡涉及兩個基本的問題,一是在程式的第二個執行個體啟動時,如何發現該程式已有一個執行個體在運行,而是如何將第一個執行個體啟用,而第二個執行個體退出。
對於第一個問題,可以通過給應用程式設定訊號量,執行個體啟動時首先檢測該訊號量,如已存在,則說明程式已運行一個執行個體。
第二個問題的痛點是擷取第一個執行個體的主窗對象指標或控制代碼,然後便可用SetForegroundWindow來啟用。雖然FindWindow函數能尋找正運行著的視窗,但該函數要求指明所尋找視窗的標題或視窗類別名,不是實現通用方法的途徑。我們可以用Win 32 SDK函數SetProp來給應用程式主窗設定一個特有的標記。用GetDesktopWindow可以擷取Windows系統主控視窗對象指標或控制代碼,所有應用程式主窗都可看成該視窗的子視窗,即可用GetWindow函數來獲得它們的對象指標或控制代碼。用Win 32 SDK函數GetProp尋找每一應用程式主窗是否包含有我們設定的特定標記便可確定它是否我們要尋找的第一個執行個體主窗。使第二個執行個體退出很簡單,只要讓其應用程式物件的InitInstance函數返回FALSE即可。此外,當主視窗退出時,應用RemoveProp函數刪除我們為其設定的標記。
下面的InitInstance、OnCreate和OnDestroy函數代碼將實現上述的操作:
BOOL CEllipseWndApp::InitInstance()
{
// 用應用程式名稱建立訊號量
HANDLE hSem = CreateSemaphore(NULL, 1, 1, m_pszExeName);
// 訊號量已存在?
// 訊號量存在,則程式已有一個執行個體運行
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
// 關閉訊號量控制代碼
CloseHandle(hSem);
// 尋找先前執行個體的主視窗
HWND hWndPrevious = ::GetWindow(::GetDesktopWindow(),
GW_CHILD);
while (::IsWindow(hWndPrevious))
{
// 檢查視窗是否有預設的標記?
// 有,則是我們尋找的主窗
if (::GetProp(hWndPrevious, m_pszExeName))
{
// 主視窗已最小化,則恢複其大小
if (::IsIconic(hWndPrevious))
::ShowWindow(hWndPrevious,
SW_RESTORE);
// 將主窗啟用
::SetForegroundWindow(hWndPrevious);
// 將主窗的對話方塊啟用
::SetForegroundWindow(
::GetLastActivePopup(hWndPrevious));
// 退出本執行個體
return FALSE;
}
// 繼續尋找下一個視窗
hWndPrevious = ::GetWindow(hWndPrevious,
GW_HWNDNEXT);
}
// 前一執行個體已存在,但找不到其主窗
// 可能出錯了
// 退出本執行個體
return FALSE;
}
AfxEnableControlContainer();
// Standard initialization
// If you are not using these features and wish to reduce the size
// of your final executable, you should remove from the following
// the specific initialization routines you do not need.
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic();// Call this when linking to MFC statically
#endif
CEllipseWndDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}
int CEllipseWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == -1)
return -1;
// 設定尋找標記
::SetProp(m_hWnd, AfxGetApp()->m_pszExeName, (HANDLE)1);
return 0;
}
void CEllipseWndDlg::OnDestroy()
{
CDialog::OnDestroy();
// 刪除尋找標記
::RemoveProp(m_hWnd, AfxGetApp()->m_pszExeName);
}
6. 使應用程式顯示為任務條通知區中的表徵圖
在Windows 95任務條的右邊有一個地區被稱為通知區域,在其中可以顯示一些應用程式的表徵圖,用按一下滑鼠其中的表徵圖一般能彈出應用程式的菜單,雙擊則能顯示應用程式的完整視窗介面。時鐘和音量控制是任務條通知區最常見的表徵圖。
任務條通知區編程可以通過Windows 95外殼編程介面函數Shell_NotifyIcon來實現,該函數在shellapi.h標頭檔中聲明,其原型如下:
WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage,
PNOTIFYICONDATA pnid);
dwMessage是對通知區表徵圖進行操作的訊息,主要有三中,如下表所示。
Shell_NotifyIcon使用的訊息
訊息
說明
NIM_ADD
在任務條通知區插入一個表徵圖
NIM_ DELETE
在任務條通知區刪除一個表徵圖
NIM_ MODIFY
對任務條通知區的表徵圖進行修改
pnid傳入一個NOTIFYICONDATA結構的指標。NOTIFYICONDATA結構聲明及各域的意義表示如下:
typedef struct _NOTIFYICONDATA { // nid
DWORD cbSize; // NOTIFYICONDATA結構的位元組數
HWND hWnd; // 處理通知區表徵圖訊息的視窗控制代碼
UINT uID; // 通知區表徵圖的ID
UINT uFlags; // 表示下述三項是否有意義的標誌
UINT uCallbackMessage; // 滑鼠點擊表徵圖所發出訊息的ID
HICON hIcon; // 表徵圖控制代碼
char szTip[64]; // 當滑鼠移到表徵圖上時顯示的提示資訊
} NOTIFYICONDATA, *PNOTIFYICONDATA;
當用Shell_NotifyIcon在任務條通知區中放置一個表徵圖時,同時也定義了一條回調訊息,當使用者用按一下滑鼠或雙擊表徵圖時,NOTIFYICONDATA結構中指定的視窗控制代碼將接受到該訊息。該訊息的lParam參數將說明滑鼠操作的方式。當應用程式退出時,應刪除任務條中的表徵圖。