win32gui_menu.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # Demonstrates some advanced menu concepts using win32gui.
  2. # This creates a taskbar icon which has some fancy menus (but note that
  3. # selecting the menu items does nothing useful - see win32gui_taskbar.py
  4. # for examples of this.
  5. # NOTE: This is a work in progress. Todo:
  6. # * The "Checked" menu items don't work correctly - I'm not sure why.
  7. # * No support for GetMenuItemInfo.
  8. # Based on Andy McKay's demo code.
  9. from win32api import *
  10. # Try and use XP features, so we get alpha-blending etc.
  11. try:
  12. from winxpgui import *
  13. except ImportError:
  14. from win32gui import *
  15. from win32gui_struct import *
  16. import win32con
  17. import sys, os
  18. import struct
  19. import array
  20. this_dir = os.path.split(sys.argv[0])[0]
  21. class MainWindow:
  22. def __init__(self):
  23. message_map = {
  24. win32con.WM_DESTROY: self.OnDestroy,
  25. win32con.WM_COMMAND: self.OnCommand,
  26. win32con.WM_USER+20 : self.OnTaskbarNotify,
  27. # owner-draw related handlers.
  28. win32con.WM_MEASUREITEM: self.OnMeasureItem,
  29. win32con.WM_DRAWITEM: self.OnDrawItem,
  30. }
  31. # Register the Window class.
  32. wc = WNDCLASS()
  33. hinst = wc.hInstance = GetModuleHandle(None)
  34. wc.lpszClassName = "PythonTaskbarDemo"
  35. wc.lpfnWndProc = message_map # could also specify a wndproc.
  36. classAtom = RegisterClass(wc)
  37. # Create the Window.
  38. style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
  39. self.hwnd = CreateWindow( classAtom, "Taskbar Demo", style, \
  40. 0, 0, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, \
  41. 0, 0, hinst, None)
  42. UpdateWindow(self.hwnd)
  43. iconPathName = os.path.abspath(os.path.join( sys.prefix, "pyc.ico" ))
  44. # py2.5 includes the .ico files in the DLLs dir for some reason.
  45. if not os.path.isfile(iconPathName):
  46. iconPathName = os.path.abspath(os.path.join( os.path.split(sys.executable)[0], "DLLs", "pyc.ico" ))
  47. if not os.path.isfile(iconPathName):
  48. # Look in the source tree.
  49. iconPathName = os.path.abspath(os.path.join( os.path.split(sys.executable)[0], "..\\PC\\pyc.ico" ))
  50. if os.path.isfile(iconPathName):
  51. icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
  52. hicon = LoadImage(hinst, iconPathName, win32con.IMAGE_ICON, 0, 0, icon_flags)
  53. else:
  54. iconPathName = None
  55. print("Can't find a Python icon file - using default")
  56. hicon = LoadIcon(0, win32con.IDI_APPLICATION)
  57. self.iconPathName = iconPathName
  58. # Load up some information about menus needed by our owner-draw code.
  59. # The font to use on the menu.
  60. ncm = SystemParametersInfo(win32con.SPI_GETNONCLIENTMETRICS)
  61. self.font_menu = CreateFontIndirect(ncm['lfMenuFont'])
  62. # spacing for our ownerdraw menus - not sure exactly what constants
  63. # should be used (and if you owner-draw all items on the menu, it
  64. # doesn't matter!)
  65. self.menu_icon_height = GetSystemMetrics(win32con.SM_CYMENU) - 4
  66. self.menu_icon_width = self.menu_icon_height
  67. self.icon_x_pad = 8 # space from end of icon to start of text.
  68. # A map we use to stash away data we need for ownerdraw. Keyed
  69. # by integer ID - that ID will be set in dwTypeData of the menu item.
  70. self.menu_item_map = {}
  71. # Finally, create the menu
  72. self.createMenu()
  73. flags = NIF_ICON | NIF_MESSAGE | NIF_TIP
  74. nid = (self.hwnd, 0, flags, win32con.WM_USER+20, hicon, "Python Demo")
  75. Shell_NotifyIcon(NIM_ADD, nid)
  76. print("Please right-click on the Python icon in the taskbar")
  77. def createMenu(self):
  78. self.hmenu = menu = CreatePopupMenu()
  79. # Create our 'Exit' item with the standard, ugly 'close' icon.
  80. item, extras = PackMENUITEMINFO(text = "Exit",
  81. hbmpItem=win32con.HBMMENU_MBAR_CLOSE,
  82. wID=1000)
  83. InsertMenuItem(menu, 0, 1, item)
  84. # Create a 'text only' menu via InsertMenuItem rather then
  85. # AppendMenu, just to prove we can!
  86. item, extras = PackMENUITEMINFO(text = "Text only item",
  87. wID=1001)
  88. InsertMenuItem(menu, 0, 1, item)
  89. load_bmp_flags=win32con.LR_LOADFROMFILE | \
  90. win32con.LR_LOADTRANSPARENT
  91. # These images are "over sized", so we load them scaled.
  92. hbmp = LoadImage(0, os.path.join(this_dir, "images/smiley.bmp"),
  93. win32con.IMAGE_BITMAP, 20, 20, load_bmp_flags)
  94. # Create a top-level menu with a bitmap
  95. item, extras = PackMENUITEMINFO(text="Menu with bitmap",
  96. hbmpItem=hbmp,
  97. wID=1002)
  98. InsertMenuItem(menu, 0, 1, item)
  99. # Owner-draw menus mainly from:
  100. # http://windowssdk.msdn.microsoft.com/en-us/library/ms647558.aspx
  101. # and:
  102. # http://www.codeguru.com/cpp/controls/menu/bitmappedmenus/article.php/c165
  103. # Create one with an icon - this is *lots* more work - we do it
  104. # owner-draw! The primary reason is to handle transparency better -
  105. # converting to a bitmap causes the background to be incorrect when
  106. # the menu item is selected. I can't see a simpler way.
  107. # First, load the icon we want to use.
  108. ico_x = GetSystemMetrics(win32con.SM_CXSMICON)
  109. ico_y = GetSystemMetrics(win32con.SM_CYSMICON)
  110. if self.iconPathName:
  111. hicon = LoadImage(0, self.iconPathName, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE)
  112. else:
  113. shell_dll = os.path.join(GetSystemDirectory(), "shell32.dll")
  114. large, small = win32gui.ExtractIconEx(shell_dll, 4, 1)
  115. hicon = small[0]
  116. DestroyIcon(large[0])
  117. # Stash away the text and hicon in our map, and add the owner-draw
  118. # item to the menu.
  119. index = 0
  120. self.menu_item_map[index] = (hicon, "Menu with owner-draw icon")
  121. item, extras = PackMENUITEMINFO(fType=win32con.MFT_OWNERDRAW,
  122. dwItemData=index,
  123. wID=1009)
  124. InsertMenuItem(menu, 0, 1, item)
  125. # Add another icon-based icon - but this time using HBMMENU_CALLBACK
  126. # in the hbmpItem elt, so we only need to draw the icon (ie, not the
  127. # text or checkmark)
  128. index = 1
  129. self.menu_item_map[index] = (hicon, None)
  130. item, extras = PackMENUITEMINFO(text="Menu with o-d icon 2",
  131. dwItemData=index,
  132. hbmpItem=win32con.HBMMENU_CALLBACK,
  133. wID=1010)
  134. InsertMenuItem(menu, 0, 1, item)
  135. # Add another icon-based icon - this time by converting
  136. # via bitmap. Note the icon background when selected is ugly :(
  137. hdcBitmap = CreateCompatibleDC(0)
  138. hdcScreen = GetDC(0)
  139. hbm = CreateCompatibleBitmap(hdcScreen, ico_x, ico_y)
  140. hbmOld = SelectObject(hdcBitmap, hbm)
  141. SetBkMode(hdcBitmap, win32con.TRANSPARENT)
  142. # Fill the background.
  143. brush = GetSysColorBrush(win32con.COLOR_MENU)
  144. FillRect(hdcBitmap, (0, 0, 16, 16), brush)
  145. # unclear if brush needs to be freed. Best clue I can find is:
  146. # "GetSysColorBrush returns a cached brush instead of allocating a new
  147. # one." - implies no DeleteObject.
  148. # draw the icon
  149. DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL)
  150. SelectObject(hdcBitmap, hbmOld)
  151. DeleteDC(hdcBitmap)
  152. item, extras = PackMENUITEMINFO(text="Menu with icon",
  153. hbmpItem=hbm.Detach(),
  154. wID=1011)
  155. InsertMenuItem(menu, 0, 1, item)
  156. # Create a sub-menu, and put a few funky ones there.
  157. self.sub_menu = sub_menu = CreatePopupMenu()
  158. # A 'checkbox' menu.
  159. item, extras = PackMENUITEMINFO(fState=win32con.MFS_CHECKED,
  160. text="Checkbox menu",
  161. hbmpItem=hbmp,
  162. wID=1003)
  163. InsertMenuItem(sub_menu, 0, 1, item)
  164. # A 'radio' menu.
  165. InsertMenu(sub_menu, 0, win32con.MF_BYPOSITION, win32con.MF_SEPARATOR, None)
  166. item, extras = PackMENUITEMINFO(fType=win32con.MFT_RADIOCHECK,
  167. fState=win32con.MFS_CHECKED,
  168. text="Checkbox menu - bullet 1",
  169. hbmpItem=hbmp,
  170. wID=1004)
  171. InsertMenuItem(sub_menu, 0, 1, item)
  172. item, extras = PackMENUITEMINFO(fType=win32con.MFT_RADIOCHECK,
  173. fState=win32con.MFS_UNCHECKED,
  174. text="Checkbox menu - bullet 2",
  175. hbmpItem=hbmp,
  176. wID=1005)
  177. InsertMenuItem(sub_menu, 0, 1, item)
  178. # And add the sub-menu to the top-level menu.
  179. item, extras = PackMENUITEMINFO(text="Sub-Menu",
  180. hSubMenu=sub_menu)
  181. InsertMenuItem(menu, 0, 1, item)
  182. # Set 'Exit' as the default option.
  183. SetMenuDefaultItem(menu, 1000, 0)
  184. def OnDestroy(self, hwnd, msg, wparam, lparam):
  185. nid = (self.hwnd, 0)
  186. Shell_NotifyIcon(NIM_DELETE, nid)
  187. PostQuitMessage(0) # Terminate the app.
  188. def OnTaskbarNotify(self, hwnd, msg, wparam, lparam):
  189. if lparam==win32con.WM_RBUTTONUP:
  190. print("You right clicked me.")
  191. # display the menu at the cursor pos.
  192. pos = GetCursorPos()
  193. SetForegroundWindow(self.hwnd)
  194. TrackPopupMenu(self.hmenu, win32con.TPM_LEFTALIGN, pos[0], pos[1], 0, self.hwnd, None)
  195. PostMessage(self.hwnd, win32con.WM_NULL, 0, 0)
  196. elif lparam==win32con.WM_LBUTTONDBLCLK:
  197. print("You double-clicked me")
  198. # find the default menu item and fire it.
  199. cmd = GetMenuDefaultItem(self.hmenu, False, 0)
  200. if cmd == -1:
  201. print("Can't find a default!")
  202. # and just pretend it came from the menu
  203. self.OnCommand(hwnd, win32con.WM_COMMAND, cmd, 0)
  204. return 1
  205. def OnCommand(self, hwnd, msg, wparam, lparam):
  206. id = LOWORD(wparam)
  207. if id == 1000:
  208. print("Goodbye")
  209. DestroyWindow(self.hwnd)
  210. elif id in (1003, 1004, 1005):
  211. # Our 'checkbox' and 'radio' items
  212. state = GetMenuState(self.sub_menu, id, win32con.MF_BYCOMMAND)
  213. if state==-1:
  214. raise RuntimeError("No item found")
  215. if state & win32con.MF_CHECKED:
  216. check_flags = win32con.MF_UNCHECKED
  217. print("Menu was checked - unchecking")
  218. else:
  219. check_flags = win32con.MF_CHECKED
  220. print("Menu was unchecked - checking")
  221. if id == 1003:
  222. # simple checkbox
  223. rc = CheckMenuItem(self.sub_menu, id,
  224. win32con.MF_BYCOMMAND | check_flags)
  225. else:
  226. # radio button - must pass the first and last IDs in the
  227. # "group", and the ID in the group that is to be selected.
  228. rc = CheckMenuRadioItem(self.sub_menu, 1004, 1005, id,
  229. win32con.MF_BYCOMMAND)
  230. # Get and check the new state - first the simple way...
  231. new_state = GetMenuState(self.sub_menu, id, win32con.MF_BYCOMMAND)
  232. if new_state & win32con.MF_CHECKED != check_flags:
  233. raise RuntimeError("The new item didn't get the new checked state!")
  234. # Now the long-winded way via GetMenuItemInfo...
  235. buf, extras = EmptyMENUITEMINFO()
  236. win32gui.GetMenuItemInfo(self.sub_menu, id, False, buf)
  237. fType, fState, wID, hSubMenu, hbmpChecked, hbmpUnchecked, \
  238. dwItemData, text, hbmpItem = UnpackMENUITEMINFO(buf)
  239. if fState & win32con.MF_CHECKED != check_flags:
  240. raise RuntimeError("The new item didn't get the new checked state!")
  241. else:
  242. print("OnCommand for ID", id)
  243. # Owner-draw related functions. We only have 1 owner-draw item, but
  244. # we pretend we have more than that :)
  245. def OnMeasureItem(self, hwnd, msg, wparam, lparam):
  246. ## Last item of MEASUREITEMSTRUCT is a ULONG_PTR
  247. fmt = "5iP"
  248. buf = PyMakeBuffer(struct.calcsize(fmt), lparam)
  249. data = struct.unpack(fmt, buf)
  250. ctlType, ctlID, itemID, itemWidth, itemHeight, itemData = data
  251. hicon, text = self.menu_item_map[itemData]
  252. if text is None:
  253. # Only drawing icon due to HBMMENU_CALLBACK
  254. cx = self.menu_icon_width
  255. cy = self.menu_icon_height
  256. else:
  257. # drawing the lot!
  258. dc = GetDC(hwnd)
  259. oldFont = SelectObject(dc, self.font_menu)
  260. cx, cy = GetTextExtentPoint32(dc, text)
  261. SelectObject(dc, oldFont)
  262. ReleaseDC(hwnd, dc)
  263. cx += GetSystemMetrics(win32con.SM_CXMENUCHECK)
  264. cx += self.menu_icon_width + self.icon_x_pad
  265. cy = GetSystemMetrics(win32con.SM_CYMENU)
  266. new_data = struct.pack(fmt, ctlType, ctlID, itemID, cx, cy, itemData)
  267. PySetMemory(lparam, new_data)
  268. return True
  269. def OnDrawItem(self, hwnd, msg, wparam, lparam):
  270. ## lparam is a DRAWITEMSTRUCT
  271. fmt = "5i2P4iP"
  272. data = struct.unpack(fmt, PyGetMemory(lparam, struct.calcsize(fmt)))
  273. ctlType, ctlID, itemID, itemAction, itemState, hwndItem, \
  274. hDC, left, top, right, bot, itemData = data
  275. rect = left, top, right, bot
  276. hicon, text = self.menu_item_map[itemData]
  277. if text is None:
  278. # This means the menu-item had HBMMENU_CALLBACK - so all we
  279. # draw is the icon. rect is the entire area we should use.
  280. DrawIconEx(hDC, left, top, hicon, right-left, bot-top,
  281. 0, 0, win32con.DI_NORMAL)
  282. else:
  283. # If the user has selected the item, use the selected
  284. # text and background colors to display the item.
  285. selected = itemState & win32con.ODS_SELECTED
  286. if selected:
  287. crText = SetTextColor(hDC, GetSysColor(win32con.COLOR_HIGHLIGHTTEXT))
  288. crBkgnd = SetBkColor(hDC, GetSysColor(win32con.COLOR_HIGHLIGHT))
  289. each_pad = self.icon_x_pad // 2
  290. x_icon = left + GetSystemMetrics(win32con.SM_CXMENUCHECK) + each_pad
  291. x_text = x_icon + self.menu_icon_width + each_pad
  292. # Draw text first, specifying a complete rect to fill - this sets
  293. # up the background (but overwrites anything else already there!)
  294. # Select the font, draw it, and restore the previous font.
  295. hfontOld = SelectObject(hDC, self.font_menu)
  296. ExtTextOut(hDC, x_text, top+2, win32con.ETO_OPAQUE, rect, text)
  297. SelectObject(hDC, hfontOld)
  298. # Icon image next. Icons are transparent - no need to handle
  299. # selection specially.
  300. DrawIconEx(hDC, x_icon, top+2, hicon,
  301. self.menu_icon_width, self.menu_icon_height,
  302. 0, 0, win32con.DI_NORMAL)
  303. # Return the text and background colors to their
  304. # normal state (not selected).
  305. if selected:
  306. SetTextColor(hDC, crText)
  307. SetBkColor(hDC, crBkgnd)
  308. def main():
  309. w=MainWindow()
  310. PumpMessages()
  311. if __name__=='__main__':
  312. main()