分享

菜单和列表框 (转)

 魔音工作室 2012-12-04

9. 菜单和列表框 (转)

(2008-07-09 12:52:49)
标签:

杂谈

分类: 计算机与 Internet

菜单
  菜单是 Windows 程序设计的基础部分,菜单的使用大大提高了用户体验,并带给用户比较相近的使用界面。菜单从菜单资源获取它们的名字,菜单资源中有一个可用项目的列表。Windows 菜单就是用户可用项目的列表,可以被单击打开一个子菜单或者使程序做某些事情。主菜单嵌入在菜单栏,子菜单从菜单栏下拉并且可以拥有其他子菜单。菜单栏有时被称作顶层菜单,子菜单有时被称作弹出菜单。菜单栏上的菜单按钮命名应该给出程序提供的命令的主要种类。Windows 还提供一种快捷菜单,它们与菜单栏无关,可以出现在屏幕的任意位置。有时这些菜单也叫做上下文菜单或者弹出菜单,它们可以通过右键点击出现。
列表框
  列表框是包含用户可选择的项目列表的控件。列表框项目表现形式可以是文本字符串、位图或两者结合。如果列表框大小不够一次容纳所有列表项目,它会提供一个滚动条,用户在需要的时候可以滚动列表框遍历项目,选择自己想要的列表项。选择一个列表项会改变它的可视化外观,通常是改变文字和背景颜色为操作系统为选择项设定的颜色。当用户选择某一项或者从某一项不再选择,系统会向列表框父窗口发送一个通知消息。通知消息可以被处理来添加列表框的附加功能。Win32 API 提供两个列表框的常规风格:单选(默认风格)和多选。
菜单
  菜单应该是一种最为公认与熟悉的计算机接口形式。给用户呈现一致的菜单界面是非常有用的。菜单栏(主菜单)在标题栏正下方显示,子菜单从主菜单下拉,其他子菜单再从这些下拉菜单呈现。菜单项可以调用一个命令或者打开一个子菜单。调用命令的菜单被称作命令项或者命令。主菜单的菜单项几乎总是打开一个子菜单,主菜单很少包含命令项,主菜单项打开子菜单是一个惯例。每个菜单都必须有一个 owner 窗口,这样当用户选择菜单或者从菜单选择一项的时候, Windows 可以发送消息给它的 owner 窗口。
创建菜单
  创建菜单有两种方法:使用菜单创建函数或使用资源模板(使用 brcc32.exe 把 .RC 资源源文件编译成 .RES 文件 )。最常用(也最简单)的方法是使用资源模板,但是首先我们要演示如何使用菜单创建函数,这样你可以在运行期创建菜单。
  一个最简单的 API 函数是 CreateMenu ,它没有参数,返回新菜单的句柄。
hMenu := CreateMenu;
新菜单完全是空的,直到你添加某些项目它是无法使用的。如果你使用空菜单句柄设置一个菜单或者在菜单创建函数中使用,不会发生任何事情。有几个向菜单添加菜单项的函数,InsertMenuItem( ) 是非常强大的,但是如果你不需要 InsertMenuItem 的额外的特征, AppendMenu( ) 和 InsertMenu( ) 也是很有用的。我将从 AppendMenu( ) 开始。
--------------------------------------------------------------------------------
AppendMenu( ) 函数
function AppendMenu(
hMenu: THandle; // 菜单的句柄
uFlags: Cardinal; // 菜单项标志
uIDNewItem: Cardinal; // 菜单项标识符或子菜单的句柄
lpNewItem: PChar // 菜单项文字
): Boolean; // 布尔型返回值
  第一参数 hMenu 是你需要添加菜单项的菜单的句柄。第二个参数 uFlags 是这个菜单项的创建标志位,在下面会列出。它们有 MF_ 前缀表示菜单标志(Menu Flag),在很多菜单创建和修改函数中都会用到。
MF_BITMAP:使用位图作为菜单项,lpNewItem 参数包含作为菜单项显示的位图的句柄。
MF_CHECKED:在菜单项前面放置一个检查标志(勾选)。这个标志显示一个检查标志位图。
MF_UNCHECKED:菜单项不放置检查标志(默认)。这个标志将会在菜单项显示一个未勾选标志(如果指定则显示,默认是什么都不显示)。
MF_DISABLED:禁用菜单项使它不能被选择,但是该标志不使之变灰。
MF_ENABLED:使得菜单项可用,能被用户选择(默认),从变灰状态恢复。
MF_GRAYED:禁用菜单项使它不能被选择,使该项变灰呈现标准的不可用外观。
MF_MENUBREAK:放置该项到新一行(菜单栏)或新一列(下拉菜单、子菜单或快捷菜单)但不使用分栏标志。
MF_OWNERDRAW:指定该项为自定义绘制项。在菜单第一次显示前,它的 owner 窗口接收一个 WM_MEASUREITEM 消息来重新得到菜单项的宽度和高度。只要菜单项的外观需要更新,就会发送 WM_DRAWITEM 消息给它的 owner 窗口。
MF_POPUP:指定菜单项打开一个下拉菜单或子菜单。lpNewItem 参数指定下拉菜单或子菜单的句柄。这个标志用于添加菜单名字到菜单栏,或者添加菜单项用于打开下拉菜单、子菜单或快捷菜单。
MF_SEPARATOR:绘制水平分隔线。这个标志只用于下拉菜单、子菜单或快捷菜单。分隔线不能变灰、禁用或者高亮显示。lpNewItem 和 uIDNewItem 参数将被忽略。
MF_STRING:指定菜单项作为文本字符串, lpNewItem 参数指向 PChar。
  由于菜单显示冲突的缘故,下列四组标志不能组合在一起使用:
1: MF_CHECKED 和 MF_UNCHECKED
2: MF_DISABLED、MF_ENABLED 和 MF_GRAYED
3: MF_BITMAP、MF_STRING 和 MF_OWNERDRAW
4: MF_MENUBARBREAK 和 MF_MENUBREAK
  下一个参数为 uIDNewItem ,它指定新菜单项的 ID 编号,或者子菜单的句柄(如果参数 uFlags 被设置为 MF_POPUP 的话)。最后一个参数为 lpNewItem ,它在 windows.pas 单元被定义为 PChar 类型,通常包含菜单项的文字。不过,如果 uFlag 位被设置成为 MF_BITMAP ,那么这个参数就是需要显示的位图的句柄,你需要把 THandle 强制转化为 PChar 。如果 uFlag 位被设置成为 MF_OWNERDRAW ,那么这个参数被作为整型值,在菜单外观需要更新时发送 WM_MEASURE 或者 WM_DRAWITEM 消息,该值将在 lParam 的 itemData 传递,这次需要把整型强制转化为 PChar。
添加加速键(热键)
  菜单的标准键盘接口可以通过向菜单项添加加速键(热键)来改善。加速键是菜单项文字中的下划线字符。当某个菜单被激活时,用户可以按下菜单项上对应的下划线字符来选择这个菜单项。用户可以通过按下 ALT 键来高亮显示菜单栏的第一项来激活菜单栏。子菜单在出现时处于激活状态。为了给菜单项创建一个加速键,只要在菜单项文本的任意字符前面加上“&”符号即可。例如,文本字符串“&Move”会使系统给字符“M”加上下划线并自动设置热键为“m”。当你使用 MF_STRING 标志时,你可以在你想设置热键的字符前面加上“&”字符。如果你想在文本中出现“&”符号,那么你需要把两个“&”符号放在一起使用,比如“&Bread && Butter”将显示“Bread & Butter”形式。例如:
  为了添加文本菜单项,使用“101”作为 ID 编号,“New”作为文字,可以使用
AppendMenu(hMenu, MF_STRING, 101, '&New');
  为了添加分隔线(最后两个参数没用),可以使用
AppendMenu(hMenu, MF_SEPARATOR, 0,nil);
  为了添加子菜单项“Folders”,第三个参数为子菜单句柄,使用
AppendMenu(hMenu, MF_POPUP or MF_STRING, hSubMenu, 'Folders');
  为了添加位图菜单项,使用“102”作为 ID 编号(必须将位图句柄强制转化为 PChar),可以使用
AppendMenu(hMenu, MF_BITMAP, 102, PChar(hBitmap));
--------------------------------------------------------------------------------
InsertMenu( ) 函数
  InsertMenu( )函数与 AppendMenu( )函数很相近,用于在已经存在的菜单上的特定位置添加菜单项。InsertMenu( )函数有一个额外的从 0 起步的参数,用于插入菜单项的位置。它在 window.pas as 单元定义如下:
function InsertMenu(
hMenu: THandle; // 菜单的句柄
uPosition, Cardinal, // 菜单项位置
uFlags: Cardinal; // 菜单项标志
uIDNewItem: Cardinal; // 菜单项标志
lpNewItem: PChar // 菜单项文字
): Boolean; // 布尔型返回值
  除了 uPosition 参数,它和 AppendMenu( )函数的参数是一致的。第二个参数 uPosition ,是基于 0 起步的你想放置菜单项的位置,如果你给出一个大于可用位置的数,它将在菜单的最后一行添加菜单项。其他参数和 AppendMenu( ) 函数一样,例如,为了在第四行添加文本菜单项,使用“203”作为 ID 编号,“Undo”作为文字,可以使用:
InsertMenu(hMenu, 3, MF_STRING, 203, '&Undo');
--------------------------------------------------------------------------------
InsertMenuItem( ) 函数
InsertMenuItem( ) 函数使用 TMenuItemInfo 记录来获取菜单项的创建信息。由于 TMenuItemInfo 记录有 11 个域,它比只有 5 个参数的 InsertMenu( ) 函数包含的信息更多,因此 InsertMenuItem( ) 比 AppendMenu 或 InsertMenu 函数拥有更多的创建选项。InsertMenuItem 在 windows.pas 单元定义如下:
function InsertMenuItem(hMenu: THandle; // 菜单的句柄
uItem: Cardinal; // ID 编号或菜单项位置
fByPosition: Boolean; // uItem 中的 ID 编号或菜单项位置
const lpmii: TMenuItemInfo // TMenuItemInfo 记录
): Boolean; // 布尔型返回值
  第一个参数 hMenu 是菜单项涉及的菜单句柄。第二个参数 uItem 是 ID 编号或从 0 起步的菜单项位置。第二个参数的意义由第三个参数 fByPosition 设置,为 True 则 uItem 表示位置号码,为 False 则 uItem 表示 ID 编号。第四个参数 lpmii 为 TMenuItemInfo 记录类型,在 windows.pas 单元定义如下:
TMenuItemInfo
PMenuItemInfoA = ^TMenuItemInfoA;
PMenuItemInfo = PMenuItemInfoA;
tagMENUITEMINFOA = packed record
cbSize: Cardinal;
fMask: Cardinal;
fType: Cardinal; { used if MIIM_TYPE}
fState: Cardinal; { used if MIIM_STATE}
wID: Cardinal; { used if MIIM_ID}
hSubMenu: HMENU; { used if MIIM_SUBMENU}
hbmpChecked: HBITMAP; { used if MIIM_CHECKMARKS}
hbmpUnchecked: HBITMAP; { used if MIIM_CHECKMARKS}
dwItemData: Cardinal; { used if MIIM_DATA}
dwTypeData: PAnsiChar; { used if MIIM_TYPE}
cch: Cardinal; { used if MIIM_TYPE}
hbmpItem: HBITMAP; { used if MIIM_BITMAP}
end;
tagMENUITEMINFO = tagMENUITEMINFOA;
TMenuItemInfoA = tagMENUITEMINFOA;
TMenuItemInfo = TMenuItemInfoA;
MENUITEMINFO = MENUITEMINFOA
  参见 Win32 API 帮助中的“MENUITEMINFO”查看这个 TMenuItemInfo 记录成员的意义。使用 TMenuItemInfo 需要花费一些时间来了解如何(以及何时)使用与不使用记录成员依赖于 fMask 和 fType 标志的设置。共有六种 fMask 标志位(MIIM_CHECKMARKS、 MIIM_DATA、MIIM_ID、MIIM_STATE、MIIM_SUBMENU 和 MIIM_TYPE),通过包含 fMask 中的标志位,标志位的信息将在 TMenuItemInfo 记录中使用。如果不设置标志位则记录成员将被忽略。例如标志位包含 MIIM_STATE ,那么成员 fState 将被使用,否则他被忽略。下面是使用 InsertMenuItem( ) 向一个菜单添加菜单项的实例:
--------------------------------------------------------------------------------
  这段代码将在菜单 hMenu1 的第一行添加一个文本菜单项,ID 编号为 700。
var
MenuInfo1: TMenuItemInfo;
begin
MenuInfo1.cbSize := SizeOf(TMenuItemInfo);
MenuInfo1.fMask := MIIM_ID or MIIM_TYPE;
MenuInfo1.fType := MFT_STRING;
MenuInfo1.wID := 700;
MenuInfo1.dwTypeData := '&Menu Item1';
{因为使用了 MIIM_ID or MIIM_TYPE 标志,下面的几行代码
将会被忽略,所以你根本不必设置。}
MenuInfo1.cch := 0;
{如果 dwTypeData 用于设置文本,那么 cch 是没有用处的,
只有 dwTypeData 用于获取文本,那么 cch 才有用。}
MenuInfo1.fState := 0;
MenuInfo1.hSubMenu := 0;
MenuInfo1.hbmpChecked := 0;
MenuInfo1.hbmpUnchecked := 0;
MenuInfo1.hbmpItem := 0;
InsertMenuItem(hMenu1, 0, True, MenuInfo1);
end;
  只要你使用 TMenuItemInfo 记录,你就必须在传递记录到函数之前设置 cbSize 为 TMenuItemInfo 的大小。 fMask 域被设置成为 MIIM_ID or MIIM_TYPE ,这将告诉 InsertMenuItem( ) 函数使用 wID 和 fType 域并忽略其他域。wID 被设置为 ID 编号 700(尽管该域被定义为 Cardinal 类型,但只有2字节是可以读取的,所以你需要设置该值低于 65535)。由于设置了 MIIM_TYPE 标志, fType 域会被读取,这里设置为 MFT_STRING ,这将使 dwTypeData 域被作为 PChar 读取。上面的代码和下面一句代码产生同样的菜单项:
InsertMenu(hMenu1, 0, MF_STRING, 700, '&Menu Item1'');
--------------------------------------------------------------------------------
  下面的代码将会产生一个菜单分隔栏:
MenuInfo1.cbSize := SizeOf(TMenuItemInfo);
MenuInfo1.fMask := MIIM_TYPE;
MenuInfo1.fType := MFT_SEPARATOR;
{由于其他域会被忽略,所以根本不必设置。}
InsertMenuItem(hMenu1, 1, True, MenuInfo1);
--------------------------------------------------------------------------------
  下面的代码将会产生一个文本菜单项,处于检测勾选状态并且灰色显示:
begin
MenuInfo1.cbSize := SizeOf(TMenuItemInfo);
MenuInfo1.fMask := MIIM_ID or MIIM_TYPE or MIIM_STATE;
MenuInfo1.fType := MFT_STRING;
MenuInfo1.wID := 302;
MenuInfo1.dwTypeData := '&Word Wrap';
MenuInfo1.fState := MFS_CHECKED or MFS_GRAYED;
MenuInfo1.hbmpChecked := 0;
MenuInfo1.hbmpUnchecked := 0;
{不需要设置其它域}
InsertMenuItem(hMenu1, 3, True, MenuInfo1);
end;
默认菜单项
  子菜单可以包含一个默认菜单项,它使用加粗类型代替正常菜单。当用户双机打开子菜单时,系统会发送命令消息给菜单的 owner 窗体然后关闭菜单,就好像默认的菜单项被单击过一样。如果没有设置默认菜单项,那么不发生任何情况,子菜单保持打开状态。你可以使用下列函数设置默认菜单项:
SetMenuDefaultItem(hMenu: THandle; // 菜单的句柄
uItem: Cardinal // ID 编号或菜单项位置
fByPos: Cardinal // 0 为 ID 编号,1 为菜单项位置
): BOOL; // 布尔型返回值
  比如设置菜单项 ID 编号 105 为默认菜单项:SetMenuDefaultItem(hMenu, 105, 0);
  如果你设置 uItem 为 -1 ,那么不会有菜单项为默认项,因为 uItem 被定义为 cardinal 类型,不允许使用负数值,因此你将不得不使用 $FFFFFFFF 。
  你可以使用函数 GetMenuDefaultItem(hMenu: THandle; fByPos, gmdiFlags: Cardinal): Cardinal; 来获取子菜单的默认菜单项,如果返回值为 $FFFFFFFF 则表示没有默认项。
菜单相关的 API 函数
  这里是下面程序使用的菜单相关的 API 函数列表。大部分函数的命名提示了函数的作用,比如 CheckMenuItem( ) 将会勾选或不勾选某个菜单项。你应该查看帮助了解这些函数的参数,同时查看本文的例子了解这些函数的用法:
function CheckMenuItem (hMenu: HMENU; uIDCheckItem, uCheck: Cardinal): Cardinal;
- - 勾选与不勾选某个菜单项
function EnableMenuItem (hMenu: HMENU; uIDEnableItem, uEnable: Cardinal): Boolean;
- - 使可用、变灰与禁用某个菜单项
function GetMenuState (hMenu: HMENU; uId, uFlags: Cardinal): Cardinal
- - 获取菜单项的状态,是否勾选、变灰、可用
function GetMenuItemCount (hMenu: HMENU): Integer;
- - 获取菜单的菜单项数目
function GetMenuString (hMenu: HMENU; uIDItem: Cardinal; lpString: PChar; nMaxCount: Integer; uFlag: Cardinal): Integer;
- - 获取菜单项的 PChar 字符串
function CheckMenuRadioItem hMenu: HMENU; First, Last, Check, Flags: Cardinal): Boolean;
- - 勾选一组菜单项的某项,类似单选按钮
function GetMenuItemID (hMenu: HMENU; nPos: Integer): Cardinal;
- - 获取菜单项的 ID 编号
GetMenuItemInfo 与 SetMenuItemInfo
GetMenuItemInfo( ) 和 SetMenuItemInfo( ) 函数可以完成上面绝大部分函数的功能。这两个函数使用 TMenuItemInfo 记录来获取与设置菜单项特征。
GetMenuItemInfo(HMENU hMenu, // 菜单的句柄
uItem: Cardinal, // ID 编号或菜单项位置
fByPosition: Boolean, // true 表示位置,false 表示 ID
lpmii: TMenuItemInfo // 使用请求信息填充记录
);
SetMenuItemInfo(HMENU hMenu, // 菜单的句柄
uItem: Cardinal, // ID 编号或菜单项位置
fByPosition: Boolean, // true 表示位置,false 表示 ID
lpmii: TMenuItemInfo // 使用记录信息设置菜单项
);
  与 InsertMenuItem( ) 函数类似,你必须设置 TMenuItemInfo 的 fMask 和 fType 域,这样函数才能知道要获取或设置哪些信息。下列代码将会获取菜单项的字符串与 ID 编号:
var
MenuInfo1: TMenuItemInfo;
Buffer: Array[0..1024] of Char;
begin
MenuInfo1.cbSize := SizeOf(TMenuItemInfo);
MenuInfo1.fMask := MIIM_ID or MIIM_TYPE;
MenuInfo1.fType := MFT_STRING;
MenuInfo1.cch := 1023;
MenuInfo1.dwTypeData := @Buffer;
GetMenuItemInfo(hMenu1, 1, True, MenuInfo1);
//MenuInfo1.wID 将会得到 ID 编号
//MenuInfo1.dwTypeData 将会得到菜单项字符串
--------------------------------------------------------------------------------
发送给 Window Proc 的菜单消息
  前面的程序中我们已经知道,当用户单击按钮时, WM_COMMAND 消息将被发送给 Window Proc , LParam 是按钮的句柄。由于按钮被认为是最基本的用户界面,按钮的单击会发送 WM_COMMAND 消息,LParam 为 0。不管是哪个主菜单、子菜单或弹出菜单被单击,LParam 都会是 0(系统菜单不发送 WM_COMMAND 消息,它发送 WM_SYSCOMMAND 消息)。如果 LParam 为 0 则表示菜单项被单击, WParam 的 LOWORD 将包含菜单项的 ID 编号。你可以测试 LOWORD(wParam) 了解哪个菜单项被单击了(参见示例中的代码)。
  菜单出现前会发送一个 WM_INITMENUPOPUP 消息,你可以使用这个消息在菜单显示、可用、改变勾选标志、添加项……之前更新菜单。只要菜单在程序里出现,它就是模式显示的,并冻结这个线程内所有代码的执行。WM_ENTERMENULOOP 消息将告知程序菜单的模式循环已经开始。WM_EXITLOOP 消息通知程序菜单的模式循环已经退出。WM_ENTERMENULOOP 和 WM_EXITLOOP 消息在本文的例子中不会涉及到。
--------------------------------------------------------------------------------
快捷菜单或弹出菜单
  快捷菜单或弹出菜单与程序的主菜单没有关联,它在主菜单之外的某处弹出,通常是由鼠标右键触发,这些也被称作上下文菜单。你可以在程序开始时创建一个弹出菜单,然后使用 TrackPopupMenu( ) 函数弹出这个菜单。或者你也可以在需要显示菜单时随时创建和销毁它,本文的例子中就是采用此方法。为了显示弹出菜单或快捷菜单,你需要调使用 TrackPopupMenu( ) 函数,它在 windows.pas 单元定义如下:
TrackPopupMenu(hMenu: HMENU; // 快捷菜单的句柄
uFlags: Cardinal; // 屏幕位置和鼠标按键标志
x: Integer; // 屏幕坐标系的水平位置
y: Integer; // 屏幕坐标系的竖直位置
nReserved: Integer; // 保留,必须取 0
hWnd: HWND; // 菜单 owner 窗体的句柄
prcRect: PRect // 指向非放弃区域的 RECT
): Boolean; // 布尔型返回值
  这个函数对弹出菜单有效,因为它在菜单消失之前(不再可见)是不返回的(结束函数)。因此在菜单被作为模式效果显示期间,该线程的所有其他代码都不会再执行。查阅帮助可以了解其他参数的进一步信息。你可以设置 uFlags ,这样菜单会被放置在相对横坐标的位置:TPM_CENTERALIGN 将把菜单放置在横坐标的中间位置,TPM_LEFTALIGN 将把菜单放置在横坐标的左侧,TPM_RIGHTALIGN 将把菜单放置在横坐标的右侧。参见程序代码中的 procedure ShowPopMenu(X, Y: Integer); 部分。
  你也可以使用 TrackPopupMenuEx( ) 函数,它是 TrackPopupMenu 函数的新版本。
列表框
  列表框是包含用户可选择的项目的垂直列表的控件。如果列表框大小不够一次容纳所有列表项目,它会提供一个滚动条,用户在需要的时候可以滚动列表框遍历项目,选择自己想要的列表项。选择一个列表项会改变它的颜色为选择项设定的选定颜色,系统会向列表框父窗口发送一个通知消息。列表框有两种常见的风格:单选(默认风格)和多选。 CreateWindow( ) 函数提供很多其他种类的列表框和窗口风格来控制列表框的外观与操作。这些风格决定了列表框项目是否排序、多列排列、可选择等。参考 Win32 API 的 CreateWindow 部分以及列表框的风格标志。你还需要阅读帮助的 List Boxes 部分浏览列表框的相关页面。
  你可以使用下列代码创建一个标准的列表框:使用字符串,排序,拥有单线边框,父窗体获取通知消息:
hListBox1 := CreateWindow('LISTBOX', nil,
WS_VISIBLE or WS_CHILD or LBS_STANDARD,
8,30,150,220,hForm1,101,hInstance,nil);
  然而,为了得到 3D 外观,我们需要在 CreateWindowEx( ) 时使用 WS_EX_CLIENTEDGE 标志:
hListBox1 := CreateWindowEx(WS_EX_CLIENTEDGE,'LISTBOX', nil,
WS_VISIBLE or WS_CHILD or LBS_HASSTRINGS or LBS_NOTIFY or LBS_SORT or WS_VSCROLL,
8,30,150,220,hForm1,101,hInstance,nil);
  与编辑框控件类似,列表框向父窗口发送通知消息。双击列表框发送 LBN_DBLCLK 消息,文字内存不够发送 LBN_ERRSPACE 消息,失去焦点发送 LBN_KILLFOCUS 消息,选择项被取消发送 LBN_SELCANCEL 消息,选择项改变前发送 LBN_SELCHANGE 消息,获得焦点发送 LBN_SETFOCUS 。这些消息都在 HIWORD(wParam) 部分发送,列表框的句柄在 lParam 中。
列表框相关消息
  共有超过 30 个列表框消息( LB_),参见 Win32 API 帮助的“Messages to List Boxes”部分查看这些消息列表及其使用。本例中使用到的消息有:LB_ADDSTRING、 LB_GETCOUNT、 LB_GETCURSEL、 LB_GETTEXT 和 LB_RESETCONTENT。你可以在 SendMessage( ) 函数中使用这些消息。它们的名字说明了自身的功能,参见程序演示代码了解这些消息的使用。
列表框通知消息
  当一个列表框发生某些事件时,它会向父窗口的 Window Proc 过程发送一个通知消息。列表框的通知消息发出时间为:用户选择、双击或取消选择项,列表框得到或失去焦点,系统没有足够列表框请求的内存。通知消息被作为 WM_COMMAND 消息发送,wParam 参数的低位包含了列表框标识符,高位包含了通知消息,lParam 参数包含了列表框窗口句柄。HIWORD(wParam) 可以包含 LBN_DBLCLK、 LBN_ERRSPACE、 LBN_KILLFOCUS、 LBN_SELCANCEL、 LBN_SELCHANGE 和 LBN_SETFOCUS。本文的程序代码中有一个使用 LBN_DBLCLK 获取列表框双击通知消息的例子。
子类化列表框
  在本文的程序中,列表框1 将改变它的选择规则为鼠标经过这个列表项,类似组合框的行为。它的实现代码在 function Listbox1Proc( ) 部分,它将列表框控件子类化。参见 Listbox1Proc( ) 部分的 if (hWnd = hListBox1) and (Msg = WM_MOUSEMOVE) 代码。列表框3 支持拖放方法,用户可以通过拖放列表项到新的位置来重排列列表项。它的实现代码在 procedure DragDropListBox; 部分,在 Listbox1Proc( ) 函数中使用鼠标按下和抬起消息。两个列表框使用同样的子类化过程 Listbox1Proc( ),如果你查看两个子类化 SetWindowLong 函数:
PListbox1Proc := Pointer(SetWindowLong(hListBox1, GWL_WNDPROC, Integer(@Listbox1Proc)));
PListbox3Proc := Pointer(SetWindowLong(hListBox3, GWL_WNDPROC, Integer(@Listbox1Proc)));
  你会发现 Listbox1Proc( ) 将作为两个列表框的新的处理过程,参见程序中的代码:
if PListbox1Proc = PListbox3Proc then SetWindowText(hEdit1, 'C:\WINDOWS');
  这个条件将返回 True ,PListbox1Proc 确实等于 PListbox3Proc 。Windows 系统对于每个标准类如“BUTTON”、“EDIT”和“LISTBOX”等有一个单独的 Window Proc 。看看 Listbox1Proc( ) 结尾调用系统默认消息处理机制的代码:
Result:=CallWindowProc(PListBox1Proc,hWnd,Msg,wParam,lParam);
  你没有必要使用下列代码:
if hWnd = hListBox1
then Result:=CallWindowProc(PListBox1Proc,hWnd,Msg,wParam,lParam)
else if hWnd = hListBox3
then Result:=CallWindowProc(PListBox3Proc,hWnd,Msg,wParam,lParam);
  因为 PListbox1Proc 和 PListbox3Proc 是等价的。
  有时为两个或两个以上的控件使用同一个 window proc 并使用 hWnd 参数得到不同控件的消息还是比较方便的。
使用文件(夹)名填充列表框
  如果你发送 LB_DIR 消息到一个列表框,你可以用 LParam 内的文件夹下面的文件夹和文件名填充这个列表框。本文使用这个方法利用文件名填充列表框1 ,但是这个方法是 Windows 3.x (16位)时期留下来的,因此你只能在列表框得到短文件名。如果你想在列表框得到 32 位的长文件名,你需要使用 FindFirstFile( ) 函数。参见下面例子的 procedure GetFiles(FilePath: String); 部分查看这个演示,列表框2 就是使用这种方法填充文件名的。
菜单和列表框程序
  下面的代码中有很多创建和使用菜单与列表框的实例,但是可能多的一次处理不完,所以你可以创建自己的 GUI 程序,利用 hForm1 主窗体然后复制这里的代码创建一个简单的菜单,在你的 Window Proc (MessageFunc) 中放置代码处理菜单点击消息。你可以复制其他代码体验菜单函数 CheckMenuItem( ) 和 GetMenuState( ) 等,查看如何使用它们以及如何给出参数使用各种选项。接下来创建一个或两个列表框,使用 LB_ADDSTRING 和 LB_GETCURSEL这样的列表框消息,你在下面的代码中可以看到实例。在你了解很多关于菜单和列表框使用的知识后,你可以添加更多的函数以及使用更多的选项。下面的过程与函数的名字如 procedure ShowPopMenu( ) 将会给出其功能与用途的暗示。
  下面的代码中有大量的注释,这些会解释相关的代码为什么会出现以及有什么用途。
program MenuListB;
uses
Windows, Messages, smallutils;
{$R *.RES}
var
wClass: TWndClass;
hForm1, menuFile, menuListB1, menuListB2, menuListB3,
menuMain, menuSubFolder1, menuSubFolder2, hListBox1,
hListBox2, hListBox3, hEdit1, Bitmap1, hSysMenu: Integer;
mainMsg: TMsg;
MenuInfo: TMenuItemInfo;
PListBox1Proc, PListBox3Proc: Pointer;
Rect1: TRect;
DlgChk, CanDrag, DoChange: Boolean;
Count, CopyNum: Integer;
UpSelect, DownSelect: Integer;
DlgEditText: string;
FindData: TWin32FindData;
ErrorMode: Cardinal;
FindHandle: THandle;
CharBuffer: array[0..1024] of Char;
TempDC, BmpDC: HDC;
const
About = '这是菜单和列表框的演示程序'#10'向你展示如何使用菜单与列表框编程';
{作为菜单项的ID编号需要使用常量,这将有助于你知道哪个菜单项使用哪个ID编号。}
mID_m1New = 101;
mID_m1Open = 102;
mID_m1Save = 103;
mID_m1SaveAs = 104;
mID_m1Show = 105;
mID_m1Exit = 106;
mID_m2NewFolder = 201;
mID_m2AddSel = 202;
mID_m2Clear = 203;
mID_m2ChangeItem = 204;
mID_m2Item = 205;
function Max(A, B: Integer): Integer;
begin
{math.pas 单元的一个函数}
if A > B then
Result := A
else
Result := B;
end;
procedure ShutDown;
begin
DeleteObject(Bitmap1);
PostQuitMessage(0);
end;
procedure ShowPopMenu(X, Y: Integer);
var
hPopMenu: HMENU;
begin
{这个过程由右键单击的 WM_CONTEXTMENU 消息来调用,它将创建并显示一个弹出菜单。}
hPopMenu := CreatePopupMenu;
{这个 CreatePopupMenu 函数创建一个空菜单}
{AppendMenu() 向菜单中添加菜单项}
AppendMenu(hPopMenu, MF_STRING, 501, '消息对话框');
{501 是单击菜单时发送给 WndProc 的 ID 编号}
AppendMenu(hPopMenu, MF_STRING, 502, '移动');
AppendMenu(hPopMenu, MF_STRING, 503, '关于');
AppendMenu(hPopMenu, MF_STRING, 504, '退出');
{只要弹出菜单一出现,TrackPopupMenu 函数就会被激活,所以在菜单弹出前该线程
内不会发生其他任何事情。 }
TrackPopupMenu(hPopMenu,
TPM_LEFTALIGN or TPM_LEFTBUTTON,
X - 5,
Y - 5,
0,
hForm1, {参见 MessageProc 中 WM_COMMAND 菜单消息部分}
nil
); {由于菜单每次出现都会创建,所以必须销毁}
DestroyMenu(hPopMenu);
end;
procedure DragDropListBox;
{列表框在拖放后改变,你需要获取文本并插入新列表项并删除旧列表项。}
var
Pos: Integer;
begin
{UpSelect 和 DwnSelect 是 ListBox1Proc fuction 函数传递来的鼠标操作}
Pos := UpSelect;
SendMessage(hListBox3, LB_GETTEXT, DownSelect, Integer(@CharBuffer));
if DownSelect < UpSelect then
Inc(UpSelect);
SendMessage(hListBox3, LB_INSERTSTRING, UpSelect, Integer(@CharBuffer));
if DownSelect > UpSelect then
Inc(DownSelect);
SendMessage(hListBox3, LB_DELETESTRING, DownSelect, 0);
SendMessage(hListBox3, LB_SETCURSEL, Pos, 0);
end;
function Listbox1Proc(hWnd, Msg, wParam, lParam: Integer): Integer; stdcall;
{这里需要得到鼠标点击消息作为拖放操作并使用鼠标移动消息来改变选择项}
var
X, Y, selNum, CurSel: Integer;
begin
//Result := 0;
if (hWnd = hListBox1) and (Msg = WM_MOUSEMOVE) then
begin
{鼠标光标在列表框1 内移动选择项}
CurSel := CallWindowProc(PListBox1Proc, hWnd, LB_GETCURSEL, 0, 0);
selNum := CallWindowProc(PListBox1Proc, hWnd, LB_ITEMFROMPOINT, 0,
MAKELPARAM(LOWORD(lParam) {X}, HIWORD(lParam) {Y}));
if CurSel <> selNum then
CallWindowProc(PListBox1Proc, hWnd, LB_SETCURSEL, selNum, 0);
{由于这是列表框的消息处理过程,你不需要调用 SendMessage ,你可以直接使用
CallWindowProc(PListBox1Proc, ……}
end;
{如果列表框3 允许拖放,则 CanDrag 设置为 true 。}
if CanDrag and (hWnd = hListBox3) then
case Msg of
{这是处理列表框3 拖放操作的代码}
WM_LBUTTONDOWN:
begin
{按键按下是获取 X 和 Y ,然后得到列表框的选择项,DownSelect
用于存储鼠标按键抬起的选择编号。}
X := LOWORD(lParam);
Y := HIWORD(lParam);
selNum := CallWindowProc(PListBox3Proc, hWnd, LB_ITEMFROMPOINT,
0, MAKELPARAM(X, Y));
{LB_ITEMFROMPOINT 获得列表框中距离鼠标指针最近的列表项编号}
if HIWORD(selNum) = 0 then
{如果鼠标指针处于列表框区域之内 则 HIWORD(selNum) 为 0 。}
begin
DownSelect := LOWORD(selNum);
{LOWORD(selNum) 保存了选择位置}
DoChange := True;
{DoChange 将防止按键在拖放事件执行之间抬起}
end;
{提示:你可以直接使用 LB_GETCURSEL 获得列表框当前选择项,无论
鼠标按键按下事件发生在何处。但是这里只是为了演示如何使用
LB_ITEMFROMPOINT。}
end;
WM_LBUTTONUP:
begin
X := LOWORD(lParam);
Y := HIWORD(lParam);
{鼠标位置由 lParam 的高位与低位给出}
selNum := CallWindowProc(PListBox3Proc, hWnd, LB_ITEMFROMPOINT,
0, MAKELPARAM(X, Y));
if X < 186 then
{X < 186 为了确保鼠标抬起时在列表框内,同样需要检测 Y 。}
begin
if DoChange and (DownSelect <> selNum) then
begin
UpSelect := selNum;
DragDropListBox;
{DragDropListBox 重现排列列表框的各项}
end;
end;
end;
end;
{if hWnd = hListBox3
then Result:=CallWindowProc(PListBox3Proc,hWnd,Msg,wParam,lParam)
else if hWnd = hListBox1
then Result:=CallWindowProc(PListBox1Proc,hWnd,Msg,wParam,lParam);}
{不需要使用上面的代码,因为 PListBox1Proc 和 PListBox3Proc 一样,系统处理所有
系统类比如"LISTBOX"的方法是一样的。}
Result := CallWindowProc(PListBox1Proc, hWnd, Msg, wParam, lParam);
end;
procedure GetFiles(FilePath: string);
var
num: Cardinal;
begin
{这将获取一个文件夹内的文件并将文件名放入列表框2 。}
{if not DirectoryExists(FilePath) then Exit;}
num := 0;
SendMessage(hListBox2, WM_SETREDRAW, 0, 0);
{WM_SETREDRAW 消息停止列表框的重绘更新,因此它在所有改变完成前不会重绘。}
SendMessage(hListBox2, LB_RESETCONTENT, 0, 0);
{使用 LB_RESETCONTENT 清空列表框。}
SetWindowText(hListBox2, @FilePath[1]);
{存储文件夹路径在列表框的文字缓冲区内,因为这个控件的文字缓冲区是不显示的。}
FilePath := FilePath + '*.*';
ErrorMode := SetErrorMode(SEM_FailCriticalErrors);
FindHandle := FindFirstFile(@FilePath[1], FindData);
SetErrorMode(ErrorMode);
if FindHandle <> INVALID_HANDLE_VALUE then
begin
if (FindData.cFileName[0] <> '.') and (FindData.cFileName[1] <> #0) then
begin
SendMessage(hListBox2, LB_ADDSTRING, 0,
Integer(@FindData.cFileName));
Inc(num);
end;
while FindNextFile(FindHandle, FindData) do
begin
if (FindData.cFileName <> '.'#0) and (FindData.cFileName <> '..') then
begin
if (FindData.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY) then
begin
{如果找到文件夹,则在前面加上 "1dir " 来标识它是文件夹。}
SendMessage(hListBox2, LB_ADDSTRING, 0,
Integer(PChar('1dir ' + string(FindData.cFileName))));
{LB_SETITEMDATA 将设置一个整型数值作为数据。列表框的每一项都有它自己的
数据整数,这里设置为 12 表示一个文件夹,因此双击列表框时可以知道哪些项
是文件夹。你可以在这个数据中放置指针,指向 PChar、TRect 或其他你需要的
变量。}
SendMessage(hListBox2, LB_SETITEMDATA, num, 12);
Inc(num);
end
else
begin
SendMessage(hListBox2, LB_ADDSTRING, 0, Integer(@FindData.cFileName));
Inc(num);
end;
end;
end;
FindClose(FindHandle);
end;
if Num = 0 then
SendMessage(hListBox2, LB_ADDSTRING, 0,
Integer(PChar('No files found')));
SendMessage(hListBox2, WM_SETREDRAW, 1, 0);
{WM_SETREDRAW 消息的 wParam 为 1,通知列表框更新自己的外观并重绘。}
end;
procedure SetSubMenu(subMenu: Integer);
var
Num, ID, i: Integer;
begin
{每次 menuSubFolder2 子菜单显示的时候,它使用 C 盘的所有文件夹更新自己的外观。
menuSubFolder1 则不一样,它在程序开始的时候创建,并不会随着 C 盘文件夹的改变
而更新显示。}
Num := GetMenuItemCount(subMenu);
if subMenu = menuSubFolder1 then
ID := 701
else
ID := 801;
if Num > 0 then
{子菜单也可能有菜单项,因此你需要删除所有旧的菜单项。}
for i := Num - 1 downto 0 do
DeleteMenu(subMenu, i, MF_BYPOSITION);
Count := 0;
ErrorMode := SetErrorMode(SEM_FailCriticalErrors);
{设置错误模式为 SEM_FailCriticalErrors,防止 NT 系统显示磁盘不可用的错误信息。}
FindHandle := FindFirstFile('C:\*.*', FindData);
{FindFirstFile 将会得到 C:\ 的第一个文件或文件夹}
SetErrorMode(ErrorMode);
if FindHandle <> INVALID_HANDLE_VALUE then
begin
{检测 FILE_ATTRIBUTE_DIRECTORY 属性确保只得到文件夹}
if (FindData.cFileName <> '.'#0) and
(FindData.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY) then
begin
AppendMenu(subMenu, MF_STRING, ID, FindData.cFileName);
Inc(Count);
end;
while FindNextFile(FindHandle, FindData) do
begin
if (FindData.cFileName <> '.'#0) and (FindData.cFileName <> '..') and
(FindData.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY) then
begin
{由于硬盘上可能存在很多文件夹,这里限制文件夹名字长度为 70 个字符,限制
显示到菜单的文件夹数目为 63 。}
if Count > 63 then
Break;
if (StrLen(FindData.cFileName) < 70) then
begin
AppendMenu(subMenu, MF_STRING, ID + Count, FindData.cFileName);
Inc(Count);
end;
end;
end;
FindClose(FindHandle);
end;
end;
procedure SelToLB3(LB1: Boolean);
var
SMResult: Integer;
hLB: Integer;
begin
{这个过程将从列表框获取选择项的文本并将它们添加到列表框3 的项目中。}
if LB1 then
hLB := hListBox1
else
hLB := hListBox2;
SMResult := SendMessage(hLB, LB_GETCURSEL, 0, 0);
if SMResult = LB_ERR then
begin
{如果没有选择项,LB_GETCURSEL 将会返回 LB_ERR}
MessageBox(hForm1, '没有选择列表项,请选择一项并重试!',
'未选择', MB_OK or MB_ICONERROR);
Exit;
end;
SMResult := SendMessage(hLB, LB_GETTEXT, SMResult, Integer(@CharBuffer));
if SMResult < 1 then
begin
{LB_GETTEXT 返回值将是选择项文本的字符数目}
MessageBox(hForm1, '不能从列表框选择项中获取文本',
'无文本', MB_OK or MB_ICONERROR);
Exit;
end;
SendMessage(hListBox3, LB_ADDSTRING, 0, Integer(@CharBuffer));
end;
procedure DoubleClick(LBHandle: Cardinal);
var
SMResult, sel: Integer;
PathStr1: string;
begin
{列表框被双击时调用这个过程}
sel := SendMessage(LBHandle, LB_GETCURSEL, 0, 0);
if sel < LB_ERR then
Exit;
SMResult := SendMessage(LBHandle, LB_GETTEXT, sel, Integer(@CharBuffer));
if SMResult < 1 then
Exit;
PathStr1 := GetWindowStr(LBHandle);
{文件夹路径存储在列表框的文字缓冲区了}
{hListBox2 中所有文件夹的列表项的数据整数值被设置为 12,这样如果项目数据为
12 则在列表框中列出这个文件夹内的文件。}
if SendMessage(LBHandle, LB_GETITEMDATA, sel, 0) = 12 then
begin
PathStr1 := PathStr1 + PChar(@CharBuffer[6]) + '\';
//if DirectoryExists(FilePath)
GetFiles(PathStr1);
end
else
begin
if CharBuffer = 'No files found' then
PathStr1 := CharBuffer
else
PathStr1 := PathStr1 + CharBuffer;
MessageBox(hForm1, @PathStr1[1], 'File or Folder Selected', MB_OK or
MB_ICONINFORMATION);
end;
end;
procedure SortListBox;
{这个过程切换列表框3 为排序与不排序状态}
var
TextLen, ItemLen, CurSel, Count, maxLen, Style, i: Integer;
Buffer, PItems, Buf2, Pos1: PChar;
procedure CreateLB;
begin
{不能使用 SetWindowLong( ) 改变列表框的 LBS_SORT 风格,你需要销毁列表框
并使用 LBS_SORT 标志重新创建它。}
SetWindowLong(hListBox3, GWL_WNDPROC, Integer(PListbox3Proc));
DestroyWindow(hListBox3);
hListBox3 := CreateWindowEx(WS_EX_CLIENTEDGE, 'LISTBOX', Buffer,
Style, 338, 30, 188, 220, hForm1, 0, hInstance, nil);
SendMessage(hListBox3, WM_SETFONT, GetStockObject(ANSI_FIXED_FONT), 0);
PListbox3Proc := Pointer(SetWindowLong(hListBox3, GWL_WNDPROC,
Integer(@Listbox1Proc)));
end;
begin
{获取列表框3 的风格并测试其是否有 LBS_SORT 标志}
Style := GetWindowLong(hListBox3, GWL_STYLE);
if Style or LBS_SORT = Style then
begin
Style := Style and not LBS_SORT;
{移除风格中的 LBS_SORT 标志}
CanDrag := True;
CheckMenuRadioItem(menuListB3, 0, 1, 1, MF_BYPOSITION);
{单选菜单项}
end
else
begin
Style := Style or LBS_SORT;
canDrag := False;
CheckMenuRadioItem(menuListB3, 0, 1, 0, MF_BYPOSITION);
end;
{列表框3 将会销毁再重建,因此你需要从列表框获取所有字符串数据并放置
到新列表框中。}
TextLen := SendMessage(hListBox3, WM_GETTEXTLENGTH, 0, 0);
if TextLen > 0 then
begin
GetMem(Buffer, TextLen + 1);
SendMessage(hListBox3, WM_GETTEXT, TextLen + 1, Integer(Buffer));
//MessageBox(hForm1,Buffer,'Sort List Box',MB_OK or MB_ICONINFORMATION);
end;
Count := SendMessage(hListBox3, LB_GETCOUNT, 0, 0);
if Count = LB_ERR then
Count := 0;
if Count > 0 then
begin
{如果 Count 大于 0,那么你需要从列表框获取所有字符串数据并放置
到新列表框中。}
CurSel := SendMessage(hListBox3, LB_GETCURSEL, 0, 0);
ItemLen := 0;
MaxLen := 0;
for i := 0 to Count - 1 do
begin
{这里保存所有列表项文本字符串到一个 PChar 变量 PItems 中,因此需要获取
每个列表项的长度并将它们加在一起。}
TextLen := SendMessage(hListBox3, LB_GETTEXTLEN, i, 0);
Inc(ItemLen, TextLen + 1); // 为字符 #0 加 1
MaxLen := Max(MaxLen, TextLen + 1);
{这里需要 Buf2 足够大来容纳最长的列表项字符串,因此要把长度最大值保存
到 MaxLen 中。}
end;
PItems := AllocMem(ItemLen + 1);
{获取足够内存容纳所有字符。ItemLen+1 将会给 PItems 添加一个额外的字符 #0
位置用于处理后面的 repeat 循环。使用 AllocMem 是因为它利用 #0 填充 PItems
的内存区域。}
Buf2 := AllocMem(MaxLen);
try
Pos1 := PItems - 1;
{Pos1 用于指针运算}
for i := 0 to Count - 1 do
begin
{通过获取文本存入 Buf2 再使用 StrECopy 从 Buf2 复制到 PItems,这个
for 循环复制了所有的列表项到 PItems PChar 中,Pos1+1 更新到字符串
的结尾,因此每个字符串之间有 #0 分界。}
TextLen := SendMessage(hListBox3, LB_GETTEXT, i, Integer(Buf2));
if TextLen > 0 then
Pos1 := StrECopy(Ptr(Integer(Pos1) + 1), Buf2);
{StrECopy 返回指针到字符串结尾的 #0 字符。}
end;
CreateLB;
{CreateLB 将会销毁旧的列表框并创建新的列表框}
Pos1 := PItems;
repeat
{repeat 循环添加字符串到新列表框中,PItems 内存模块中有多个 #0 结尾的
字符串。通过 StrEnd 更新 Pos1,Pos1 的指针内存地址移动到 #0 分界符后
面,也就是下一个字符串的开始处。}
SendMessage(hListBox3, LB_ADDSTRING, 0, Integer(Pos1));
Pos1 := StrEnd(Pos1) + 1;
until Pos1^ = #0;
{这里创建 PItems 比需要的长度还长一个字符位置给 #0 终结符,这样字符串结尾
就有两个 #0。使用 Pos1^ 因为编译器将在 Pos1 的内存地址处读取它作为单个字
符。如果字符串的第一个字符为 #0 则循环将会停止,这就是 PItems 的末尾。}
if CurSel <> LB_ERR then
SendMessage(hListBox3, LB_SETCURSEL, CurSel, 0);
finally
FreeMem(PItems);
FreeMem(Buf2);
end;
end
else
CreateLB;
if Buffer <> nil then
FreeMem(Buffer);
{SetWindowLong(hListBox3, GWL_STYLE, WS_VISIBLE or WS_CHILD or
LBS_HASSTRINGS or LBS_NOTIFY or WS_VSCROLL or LBS_SORT);}
{上面的 SetWindowLong 使用 LBS_SORT 风格没有任何效果,因为绝大部分控件不允许
在创建之后改变风格。}
end;
未完

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多