ETH官方钱包

前往
大廳
主題

【程式】GUI元件——按鈕類 (Windows API)

Shark | 2023-07-28 09:49:20 | 巴幣 2004 | 人氣 661

之前介紹頂層視窗,這篇開始介紹各種GUI元件的建立方法和事件處理。Gtk把GUI元件稱為widget,但Windows API稱為control(控制項(xiàng))。

各種GUI系統(tǒng)有很多共通部分,每個(gè)GUI系統(tǒng)在製作時(shí)都會參考現(xiàn)有的系統(tǒng),做成大家已經(jīng)習(xí)慣的方式比較容易讓人接受。像是check box是多個(gè)是非題,radio button是多選一,在每個(gè)系統(tǒng)上都是如此。
這是一些GUI系統(tǒng)的元件一覽,即使不是用這些系統(tǒng)寫程式也可以參考。
Gtk 4  Java Swing  HTML <input> tag

按鈕雖然是比較簡單的control,但是本篇要介紹一些寫GUI程式必要的知識,篇幅比較長。

前篇:如何建一個(gè)視窗—Windows API篇
按鈕類Gtk篇
Shark的程式教學(xué)目錄



buttoncontrol.c
#define UNICODE
#include<windows.h>
#include<commctrl.h>
#include<windowsx.h> //使用Button_開頭的macro
#include<stdio.h>
#include<stdint.h>


const int BUTTON_W=120;
const int BUTTON_H=30;
const int BUTTON_OFSX=130;
const int BUTTON_OFSY=40;

LRESULT CALLBACK wndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){
  switch(message){
  case WM_COMMAND:{
    uint32_t notifyCode=HIWORD(wparam);
    if(notifyCode==BN_CLICKED){
      uint32_t buttonID=LOWORD(wparam);
      uint32_t checked=Button_GetCheck((HWND)lparam);
      printf("button %u clicked. checked %u\n",buttonID, checked);
    }
    }return 0;
  case WM_NOTIFY:{
    NMHDR* nmhdr=(NMHDR*)lparam;
    if(nmhdr->code==BCN_DROPDOWN){
      NMBCDROPDOWN* nmhdr2=(NMBCDROPDOWN*)lparam;
      printf("split button dropdown id=%llu\n", nmhdr2->hdr.idFrom);
    }
    }return 0;
  case WM_DESTROY:
    PostQuitMessage(0);
    return 0;
  }
  return DefWindowProc(hwnd,message,wparam,lparam);
}

static HWND createButton(int id, WCHAR* text, DWORD style,
    int x,int y,int w,int h, HWND parent){
  HWND hwnd=CreateWindow(WC_BUTTON, text, style|WS_CHILD|WS_VISIBLE,x,y,w,h,
    parent,NULL,NULL,NULL);
  SetWindowLongPtr(hwnd, GWLP_ID, id);
  return hwnd;
}

int main(){
  INITCOMMONCONTROLSEX icc={sizeof(INITCOMMONCONTROLSEX), ICC_STANDARD_CLASSES};
  InitCommonControlsEx(&icc);

  WNDCLASS wndclass;
  ZeroMemory(&wndclass, sizeof(WNDCLASS));
  wndclass.style=CS_HREDRAW|CS_VREDRAW;
  wndclass.lpfnWndProc=wndProc;
  wndclass.lpszClassName=L"window";
  wndclass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1);
  wndclass.hCursor=LoadCursor(NULL,IDC_ARROW);
  RegisterClass(&wndclass);

  RECT rect={0,0, BUTTON_W*3+40, 400};
  AdjustWindowRect(&rect, WS_CAPTION|WS_SYSMENU|WS_VISIBLE, 0);
  HWND window=CreateWindow(L"window", L"button control",
    WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE, CW_USEDEFAULT,CW_USEDEFAULT,
    rect.right-rect.left, rect.bottom-rect.top,
    NULL,NULL,NULL,NULL);

  //產(chǎn)生按鈕
  HWND pushButton1= createButton(1, L"(1) push button", 0,
    10,10,BUTTON_W,BUTTON_H, window);

  HWND checkbox1= createButton(2, L"(2) check box", BS_AUTOCHECKBOX,
    10, 10+BUTTON_OFSY,BUTTON_W,BUTTON_H, window);
  HWND checkbox2= createButton(3, L"(3) check box", BS_AUTOCHECKBOX,
    10, 10+BUTTON_OFSY*2,BUTTON_W,BUTTON_H, window);
  HWND checkbox3= createButton(4, L"(4) check box", BS_AUTOCHECKBOX|BS_PUSHLIKE,
    10, 10+BUTTON_OFSY*3,BUTTON_W,BUTTON_H, window);

  HWND radio1= createButton(5, L"(5) radioA", BS_AUTORADIOBUTTON|WS_GROUP,
    10+BUTTON_OFSX, 10+BUTTON_OFSY,BUTTON_W,BUTTON_H, window);
  HWND radio2= createButton(6, L"(6) radioA", BS_AUTORADIOBUTTON,
    10+BUTTON_OFSX, 10+BUTTON_OFSY*2,BUTTON_W,BUTTON_H, window);
  HWND radio3= createButton(7, L"(7) radioA", BS_AUTORADIOBUTTON|BS_PUSHLIKE,
    10+BUTTON_OFSX, 10+BUTTON_OFSY*3,BUTTON_W,BUTTON_H, window);

  HWND radio4= createButton(8, L"(8) radioB", BS_AUTORADIOBUTTON|WS_GROUP,
    10+BUTTON_OFSX*2, 10+BUTTON_OFSY,BUTTON_W,BUTTON_H, window);
  HWND radio5= createButton(9, L"(9) radioB", BS_AUTORADIOBUTTON,
    10+BUTTON_OFSX*2, 10+BUTTON_OFSY*2,BUTTON_W,BUTTON_H, window);
  HWND radio6= createButton(10, L"(10) radioB", BS_AUTORADIOBUTTON|BS_PUSHLIKE,
    10+BUTTON_OFSX*2, 10+BUTTON_OFSY*3,BUTTON_W,BUTTON_H, window);

  HWND threeStateCheckbox= createButton(11, L"(11) 3-state", BS_AUTO3STATE,
    10, 10+BUTTON_OFSY*5,BUTTON_W,BUTTON_H, window);

  HWND pushButton2= createButton(12, L"(12) defbutton", BS_DEFPUSHBUTTON,
    10+BUTTON_OFSX, 10+BUTTON_OFSY*5,BUTTON_W,BUTTON_H, window);
  HWND pushButton3= createButton(13, L"(13) pushbox", BS_PUSHBOX,
    10+BUTTON_OFSX*2, 10+BUTTON_OFSY*5,BUTTON_W,BUTTON_H, window);

  HWND commandLink= createButton(14, L"(14) command link", BS_COMMANDLINK,
    10, 10+BUTTON_OFSY*6,BUTTON_W*2,BUTTON_H*2, window);
  Button_SetNote(commandLink, L"note");
  HWND splitButton= createButton(15, L"(15) split button", BS_SPLITBUTTON,
    10, 10+BUTTON_OFSY*8,BUTTON_W*2,BUTTON_H, window);

  MSG msg;
  while(GetMessage(&msg,NULL,0,0)>0){
    DispatchMessage(&msg);
  }
  return 0;
}
增加一個(gè)header:commctrl.h,linker參數(shù)增加一個(gè)library:comctl32.lib。這兩個(gè)檔名是common controls的縮寫。
還有windowsx.h,它裡面定義一些方便的macro。

首先按照「如何建一個(gè)視窗—Windows API篇」建好頂層視窗。在建立控制項(xiàng)之前呼叫InitCommonControlsEx()將comctl32.dll初始化。
InitCommonControlsEx()說明  struct INITCOMMONCONTROLSEX說明
參數(shù)是宣告一個(gè)struct再把它的指標(biāo)傳入。struct第一個(gè)成員是struct的大小,這是Windows API常見的用法,因?yàn)樾掳娴膕truct往往會追加項(xiàng)目,函式裡可以根據(jù)struct大小判斷版本做不同處理。第二個(gè)成員是要初始化哪些控制項(xiàng),本篇只用到ICC_STANDARD_CLASSES。

之後的部分暫不解釋,先編譯出來看看,用這個(gè)指令
cl buttoncontrol.c /Febuttoncontrol.exe /O2 /MD /link user32.lib comctl32.lib

執(zhí)行的樣子




可以看到三個(gè)問題
  1. 外觀是Windows 95的風(fēng)格,跟其他程式的外觀不合。
  2. (14)command link和(15)split button沒出現(xiàn),因?yàn)檫@兩個(gè)是Windows Vista以後新增的。
  3. 字型也跟其他程式不一樣。
前兩個(gè)問題我是參考這一篇:Microsoft Learn: Enabling Visual Styles,然後另外找一些資料找到解法。
準(zhǔn)備下面兩個(gè)純文字檔放在同一資料夾。
manifest.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0"
processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
</assembly>
manifest.rc
#include<winuser.h>
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "manifest.xml"

打這個(gè)指令編譯。
rc manifest.rc
這是「Visual C++的命令列工具」提到的resource compiler。resource是Windows exe檔的特有功能,可以把資料嵌入exe檔。
會產(chǎn)生manifest.res,把它放在跟.c同一資料夾。

文件裡有提到其他方法,但我認(rèn)為做成resource嵌入可執(zhí)行檔最省事。

問題3要修改四個(gè)地方
//1.增加一個(gè)全域變數(shù)
HFONT systemFont;

//2.createButton()裡增加一行SendMessage()設(shè)定字型
static HWND createButton(int id, WCHAR* text, DWORD style,
    int x,int y,int w,int h, HWND parent){
  HWND hwnd=CreateWindow(WC_BUTTON, text, style|WS_CHILD|WS_VISIBLE, x,y,w,h,
    parent, NULL,NULL,NULL);
  SetWindowLongPtr(hwnd, GWLP_ID, id);
  SendMessage(hwnd, WM_SETFONT, (WPARAM)systemFont, FALSE); //增加這行
  return hwnd;
}

int main(){

  ……

  //3.在產(chǎn)生第一個(gè)button之前,用這幾行載入系統(tǒng)字型
  NONCLIENTMETRICS ncm;
  ncm.cbSize=sizeof(NONCLIENTMETRICS);
  SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &ncm, 0);
  systemFont=CreateFontIndirect(&ncm.lfMessageFont);

  ……

  MSG msg;
  while(GetMessage(&msg,NULL,0,0)>0){
    DispatchMessage(&msg);
  }
  //4.程式結(jié)束前刪除字型物件
  DeleteObject(systemFont);
  return 0;
}

linker參數(shù)增加gdi32.lib和manifest.res
cl buttoncontrol.c /Febuttoncontrol.exe /O2 /MD /link user32.lib comctl32.lib \
  gdi32.lib manifest.res
這樣執(zhí)行後,外觀跟現(xiàn)在的Windows版本一致了。

因?yàn)橐妹盍休敵鲆恍┯嵪ⅲ淌竭M(jìn)入點(diǎn)用「int main()」讓執(zhí)行時(shí)出現(xiàn)命令列視窗。



開始進(jìn)入正題:按鈕控制項(xiàng)。微軟的官方文件中與本篇有關(guān)的:
控制項(xiàng)一覽  按鈕類control的圖和說明

由於有一些程式碼是所有按鈕共通的,寫成一個(gè)函式createButton()。
static HWND createButton(int id, WCHAR* text, DWORD style,
    int x,int y,int w,int h, HWND parent){
  HWND hwnd=CreateWindow(WC_BUTTON, text, style|WS_CHILD|WS_VISIBLE, x,y,w,h,
    parent, NULL,NULL,NULL);
  SetWindowLongPtr(hwnd, GWLP_ID, id);
  SendMessage(hwnd, WM_SETFONT, (WPARAM)systemFont, FALSE);
  return hwnd;
}
產(chǎn)生按鈕的方法跟產(chǎn)生頂層視窗一樣是CreateWindow(),傳回值也是HWND型態(tài),參數(shù)如下:
1:WC_BUTTON macro。查commctrl.h會發(fā)現(xiàn)它被代換成一個(gè)字串L"Button",直接填L"Button"也可以(大小寫皆可)。
2:顯示在按鈕上的字。
3:window style,一些bit flag。因?yàn)檫@不是頂層視窗,必須有WS_CHILD,其他flag會決定這是哪一種按鈕。因?yàn)槊總€(gè)按鈕都需要WS_CHILD|WS_VISIBLE所以寫在createButton()裡面。
4~7:在視窗工作區(qū)裡的位置。
8:上層視窗的HWND。
9~11:此處沒用到,填NULL。

先不繼續(xù)講程式碼,先看看本篇程式裡的按鈕,把程式build出來操作看看。
CreateWindow()第三參數(shù)會決定按鈕種類,以下會說明style怎麼填。
Button Styles
  1. push button,圖中的(1)。官方文件裡寫要填BS_PUSHBUTTON,但查winuser.h會發(fā)現(xiàn)它的值是0,填0也可以。
    最一般的按鈕,按滑鼠按鈕時(shí)外觀變成被按下,放開滑鼠按鈕就跳起。
  2. check box(核取方塊),圖中的(2)~(4)。style=BS_AUTOCHECKBOX。
    多個(gè)是或否的選擇。每個(gè)check box各自獨(dú)立。
  3. radio button(單選按鈕) (5)~(10)。style=BS_AUTORADIOBUTTON。
    用在多個(gè)選項(xiàng)裡選一個(gè)。點(diǎn)選一個(gè)之後,群組裡其他radio button自動取消選取。
    設(shè)定群組的方式是,群組裡第一個(gè)radio button有WS_GROUP,之後建立的radio button會視為同一群組,直到下一個(gè)WS_GROUP才開始新的群組。

    (4)、(7)、(10)帶有BS_PUSHLIKE,這是讓它們的外觀像push button,但行為還是check box和radio button,按了會保持狀態(tài)而不是立刻跳起。
    查Button Styles的文件會發(fā)現(xiàn)也有名稱沒有AUTO的BS_CHECKBOX和BS_RADIOBUTTON,這些是滑鼠點(diǎn)了也不會改變狀態(tài),需要在後述的事件處理寫程式控制。

    以上三種是基本,以下是比較不常用或特殊用途的。
  4. 3-state check box (11)。style=BS_AUTO3STATE。
    有三種狀態(tài)的check box:空白、打勾、未決定。未決定的外觀有的GUI系統(tǒng)是方塊變灰色,有的是一個(gè)正方形填滿方塊。
    三種狀態(tài)各代表什麼是由作者寫程式?jīng)Q定,但要注意不能讓使用者混淆。
    跟一般的check box一樣,可以用BS_PUSHLIKE改變外觀,也有不自動改變狀態(tài)的BS_3STATE。
  5. default push button (12),style=BS_DEFPUSHBUTTON。
    邊框比較粗,用在一種特殊種類的視窗:對話框(dialog box),按鍵盤的Enter就等同按這個(gè)鈕,但本程式不是對話框所以看不出效果。
  6. push box (13),style=BS_PUSHBOX。
    平常外觀看不出是按鈕,但滑鼠點(diǎn)會有反應(yīng)。
  7. command link (14),style=BS_COMMANDLINK。
    一個(gè)箭頭加兩行字如圖,行為類似push button。除了用CreateWindow()第二參數(shù)設(shè)定主要顯示的字以外,可以用Button_SetNote()加一行小字。
  8. split button (15),style=BS_SPLITBUTTON。
    按鈕右邊有個(gè)小箭頭,通常按了箭頭就會跳出一個(gè)選單。不過「跳出選單」的動作不是建立按鈕就自動有,要寫程式控制。
文件裡還有一個(gè)BS_GROUPBOX,這是用來顯示下圖的方框,算是靜態(tài)元件而不會產(chǎn)生事件。

其他button style就請查官方文件,大多是跟其他flag做or運(yùn)算產(chǎn)生效果。

第二行SetWindowLongPtr()是設(shè)定一些整數(shù)屬性。GWLP_ID是設(shè)定ID,之後處理事件會有用。本篇在按鈕上把ID標(biāo)示出來以方便辨認(rèn)。
SetWindowLongPtrW()說明
(說明文件裡的函式名稱是SetWindowLongPtrW。之前說過,只要程式開頭有#define UNICODE,Windows API裡跟字串有關(guān)的函式和struct都會用Unicode的版本,反之會用ansi的版本。
LONG_PTR是跟該平臺指標(biāo)一樣大小的整數(shù),等同intptr_t。)

至於x,y,w,h參數(shù),Windows底層API沒有排版的功能,只能自己指定坐標(biāo)。本篇用一些常數(shù):BUTTON_W、BUTTON_H、BUTTON_OFSX、BUTTON_OFSY方便計(jì)算。
Gtk就有排版功能了,可以根據(jù)文字長短、視窗大小自動調(diào)整元件的位置大小,以後寫Gtk的教學(xué)可能會介紹。



接下來解說Windows處理GUI事件的方式。
GUI是個(gè)樹狀結(jié)構(gòu),不只Windows,其他GUI函式庫也是這樣做。

頂層視窗處理事件的函式:WndProc()是我們自己寫的,按鈕也有各自的WndProc(),是Windows內(nèi)建。

當(dāng)滑鼠點(diǎn)按鈕的時(shí)候,按鈕本身收到滑鼠的message (就是「讀取鍵盤與滑鼠輸入(Windows)」裡介紹的東西),上層視窗則收到notification(通知)。

上層視窗的WndProc()要怎麼處理通知呢?在官方文件左邊的選單找到「Button Control Notifications」:

點(diǎn)開後按「BN_CLICKED」看說明:BN_CLICKED notification code
有這一段:
  The parent window of the button receives this notification code through the WM_COMMAND message.
表示上層收到的是WM_COMMAND訊息。不過很多事件都會送出WM_COMMAND訊息,要根據(jù)notification code判斷是哪一種事件。
下面寫wparam的低位2 bytes是控制項(xiàng)ID,第3~4 bytes是notification code,可以用LOWORD()、HIWORD()這兩個(gè)macro取出。在WndProc()裡檢查message==WM_COMMAND以及HIWORD(wparam)==BN_CLICKED就可得知按鈕按下。
控制項(xiàng)ID是先前用SetWindowLongPtr()設(shè)定的值,可以藉此知道是哪個(gè)按鈕被按下。lparam是按鈕的hwnd。
再查看WM_COMMAND訊息的說明,裡面有寫「If an application processes this message, it should return zero.」,所以我們自己寫的WndProc()如果有處理這個(gè)事件要傳回0。

check box、radio button、3-state check box還需要知道目前狀態(tài)才能處理事件,但狀態(tài)沒有從wparam和lparam傳過來,要用Button_GetCheck()取得。它的說明要在左邊選單點(diǎn)開「Button Control Macros」找。
Button_GetCheck macro
裡面寫return None是錯(cuò)的,實(shí)際上會傳回BST_CHECKED、BST_UNCHECKED、BST_INDETERMINATE的其中一個(gè)。

按split button右邊的箭頭則會送BCN_DROPDOWN通知,查文件可得知上層視窗收到WM_NOTIFY訊息,這個(gè)訊息的額外資訊不一樣,lparam是指向一個(gè)NMHDR struct的指標(biāo),事件資訊存在裡面。
BCN_DROPDOWN notification code  WM_NOTIFY message  NMHDR structure
一樣很多事件都會送WM_NOTIFY訊息,要根據(jù)nmhdr->code判斷是哪一種事件,nmhdr->idFrom判斷是哪個(gè)控制項(xiàng)送出,然後把lparam轉(zhuǎn)成其他型態(tài)取出事件相關(guān)資訊(本篇的例子是struct NMBCDROPDOWN)。
「按箭頭之後跳出選單」要寫在這個(gè)事件處理裡面。



最後介紹SendMessage()函式。
本篇用到兩個(gè)Button_開頭的macro。查Button_GetCheck()的說明,會發(fā)現(xiàn)裡面有提到BM_GETCHECK message
打開windowsx.h找Button_GetCheck()的定義會發(fā)現(xiàn)它被代換成SendMessage()。
Button_GetCheck((HWND)lparam);
//代換成這樣
SendMessage((HWND)lparam, BM_GETCHECK, 0,0);
Button_SetNote()也是被代換成BCM_SETNOTE message。此外設(shè)定字型時(shí)也是用SendMessage()送出WM_SETFONT message

Windows API裡取得控制項(xiàng)的資訊或是修改控制項(xiàng)屬性,很多時(shí)候是用SendMessage()。第一參數(shù)是控制項(xiàng)的hwnd,第二參數(shù)是一個(gè)整數(shù)代表要做哪種操作,第三和四參數(shù)是額外資訊,隨操作種類而異。
SendMessage()說明
例如BM_SETCHECK訊息可以改變check box或radio button的狀態(tài),有興趣可以試試。

至於設(shè)定字型用的幾個(gè)函式,在此不詳細(xì)解釋。SystemParametersInfo()可以取得或修改一些系統(tǒng)資訊,CreateFontIndirect()和DeleteObject()跟Windows的一個(gè)系統(tǒng):GDI有關(guān)。
SystemParametersInfoW()說明  CreateFontIndirectW()說明  DeleteObject()說明

創(chuàng)作回應(yīng)

更多創(chuàng)作