-
-
Notifications
You must be signed in to change notification settings - Fork 798
Icon support for all columns in Winforms Table #4164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
freakboy3742
merged 15 commits into
beeware:main
from
Oliver-Leigh:winforms-table-multi-column-icons
Feb 17, 2026
Merged
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
40ffd95
Icon support for all columns in Winforms Table
Oliver-Leigh b625a9e
Added change note
Oliver-Leigh 465c0da
Fixed change note whitespace
Oliver-Leigh e419fe3
Bug fix: Dispose table when ref count is zero
Oliver-Leigh 3793d08
Changed the Table test: assert_cell_content
Oliver-Leigh 712b63f
Added a comment for the Table __del__() method
Oliver-Leigh 9b5e3ed
Addressed the WinForms handle destroyed event
Oliver-Leigh 19bdca5
PushMessage calls changed to SendMessage
Oliver-Leigh fffb3af
Moved Windows API definitions to lib files
Oliver-Leigh 0c3df09
Fixed formatting.
Oliver-Leigh eb5d60c
Update changes/4164.feature.md
Oliver-Leigh df63454
Restructured libs and added SendMessageW argtypes
Oliver-Leigh dbcc149
Fixed multiline text input after previous changes
Oliver-Leigh 60a1282
Simplify changenote.
freakboy3742 d83bba3
Remove documentation note about icons on Winforms tables.
freakboy3742 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| The Table widget on WinForms can now display icons in all columns. Previously, icons were only supported in the first column. Changes to the tests have also been made to test the new structure. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from ctypes import windll | ||
| from ctypes.wintypes import BOOL, HWND, LPARAM, UINT, WPARAM | ||
|
|
||
| from .comctl32classes import SUBCLASSPROC | ||
| from .win32 import DWORD_PTR, LRESULT, UINT_PTR | ||
|
|
||
| comctl32 = windll.comctl32 | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-defsubclassproc | ||
| DefSubclassProc = comctl32.DefSubclassProc | ||
| DefSubclassProc.restype = LRESULT | ||
| DefSubclassProc.argtypes = [HWND, UINT, WPARAM, LPARAM] | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass | ||
| RemoveWindowSubclass = comctl32.RemoveWindowSubclass | ||
| RemoveWindowSubclass.restype = BOOL | ||
| RemoveWindowSubclass.argtypes = [HWND, SUBCLASSPROC, UINT_PTR] | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass | ||
| SetWindowSubclass = comctl32.SetWindowSubclass | ||
| SetWindowSubclass.restype = BOOL | ||
| SetWindowSubclass.argtypes = [HWND, SUBCLASSPROC, UINT_PTR, DWORD_PTR] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from ctypes import ( | ||
| WINFUNCTYPE, | ||
| Structure as c_Structure, | ||
| ) | ||
| from ctypes.wintypes import HWND, INT, LPARAM, LPWSTR, UINT, WPARAM | ||
|
|
||
| from .win32 import DWORD_PTR, INT_PTR, LRESULT, PUINT, UINT_PTR | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-lvitemw | ||
| class LVITEMW(c_Structure): | ||
| _fields_ = [ | ||
| ("uiMask", UINT), | ||
| ("iItem", INT), | ||
| ("iSubItem", INT), | ||
| ("state", UINT), | ||
| ("stateMask", UINT), | ||
| ("pszText", LPWSTR), | ||
| ("cchTextMax", INT), | ||
| ("iImage", INT), | ||
| ("lParam", LPARAM), | ||
| ("iIndent", INT), | ||
| ("iGroupId", INT), | ||
| ("cColumns", UINT), | ||
| ("puColumns", PUINT), | ||
| ("piColFmt", INT_PTR), | ||
| ("iGroup", INT), | ||
| ] | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-nmhdr | ||
| class NMHDR(c_Structure): | ||
| _fields_ = [ | ||
| ("hwndFrom", HWND), | ||
| ("idFrom", UINT_PTR), | ||
| ("code", UINT), | ||
| ] | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-nmlvdispinfow | ||
| class NMLVDISPINFOW(c_Structure): | ||
| _fields_ = [ | ||
| ("hdr", NMHDR), | ||
| ("item", LVITEMW), | ||
| ] | ||
|
|
||
|
|
||
| # https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nc-commctrl-subclassproc | ||
| SUBCLASSPROC = WINFUNCTYPE( | ||
| # Return type: | ||
| LRESULT, | ||
| # Argument types: | ||
| HWND, | ||
| UINT, | ||
| WPARAM, | ||
| LPARAM, | ||
| UINT_PTR, | ||
| DWORD_PTR, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from ctypes import c_size_t | ||
| from ctypes.wintypes import LPARAM | ||
|
|
||
| LRESULT = LPARAM # LPARAM is essentially equivalent to LRESULT | ||
| UINT_PTR = c_size_t | ||
| DWORD_PTR = c_size_t | ||
| PUINT = c_size_t | ||
| INT_PTR = c_size_t |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Window constants | ||
|
|
||
| # Edit Messages | ||
| EM_SETCUEBANNER = 0x1501 | ||
|
|
||
| # List-View Item Format | ||
| LVIF_TEXT = 0x0001 | ||
| LVIF_IMAGE = 0x0002 | ||
| LVIF_STATE = 0x0008 | ||
|
|
||
| # List-View Management | ||
| LVM_GETEXTENDEDLISTVIEWSTYLE = 0x1037 | ||
| LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1036 | ||
|
|
||
| # List-View Notification | ||
| LVN_GETDISPINFOW = 0xFFFFFF4F | ||
|
|
||
| # List-View Styles (Extended) | ||
| LVS_EX_SUBITEMIMAGES = 0x2 | ||
|
|
||
| # Window Message | ||
| WM_NCDESTROY = 0x0082 | ||
| WM_REFLECT_NOTIFY = 0x204E |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,16 @@ | ||
| from ctypes import POINTER, cast | ||
| from ctypes.wintypes import HWND, LPARAM, UINT, WPARAM | ||
| from warnings import warn | ||
|
|
||
| import System.Windows.Forms as WinForms | ||
|
|
||
| from toga.handlers import WeakrefCallable | ||
|
|
||
| from ..libs import windowconstants as wc | ||
| from ..libs.comctl32 import DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass | ||
| from ..libs.comctl32classes import LVITEMW, NMHDR, NMLVDISPINFOW, SUBCLASSPROC | ||
| from ..libs.user32 import SendMessageW | ||
| from ..libs.win32 import LRESULT | ||
| from .base import Widget | ||
|
|
||
|
|
||
|
|
@@ -25,9 +32,24 @@ def _multiple_select(self): | |
| def _data(self): | ||
| return self.interface.data | ||
|
|
||
| def __del__(self): | ||
|
Oliver-Leigh marked this conversation as resolved.
|
||
| # The object self.pfn_subclass is a python class and is part of the native | ||
| # Windows process. When a Table is removed by the python GC, self.pfn_subclass | ||
| # is also removed and the Windows process has a dangling pointer. Calling | ||
| # Dispose() here fixes the problem by removing the self.pfn_subclass from the | ||
| # Windows process. | ||
| self.native.Dispose() | ||
|
|
||
| def create(self): | ||
| self.pfn_subclass = SUBCLASSPROC(self._subclass_proc) | ||
| self.native = WinForms.ListView() | ||
| self._set_subclass() | ||
|
|
||
| self.native.HandleCreated += WeakrefCallable(self.handle_created) | ||
| self.native.HandleDestroyed += WeakrefCallable(self.handle_destroyed) | ||
|
|
||
| self.native.View = WinForms.View.Details | ||
| self._enable_multi_icon_style() | ||
| self._cache = [] | ||
| self._first_item = 0 | ||
| self._pending_resize = True | ||
|
|
@@ -67,6 +89,68 @@ def create(self): | |
| ) | ||
| self.add_action_events() | ||
|
|
||
| def _enable_multi_icon_style(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is only called once. Would it be too tedious to inline it into where this is called? P.S. same applies for _set_subitem_icon |
||
| list_view_handle = int(self.native.Handle.ToString()) | ||
|
|
||
| # Use SendMessage over PostMessage since the ListView object is on the same | ||
| # thread as the messaging call. | ||
| old_style = SendMessageW( | ||
| list_view_handle, wc.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0 | ||
| ) | ||
| new_style = old_style | wc.LVS_EX_SUBITEMIMAGES | ||
|
|
||
| SendMessageW(list_view_handle, wc.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, new_style) | ||
|
|
||
| def handle_created(self, sender, e): | ||
| self._set_subclass() | ||
|
|
||
| def handle_destroyed(self, sender, e): | ||
| # Remove the subclass when a handle is destroyed to prevent a memory leak. | ||
| self._remove_subclass() | ||
|
|
||
| def _set_subclass(self): | ||
| SetWindowSubclass(int(self.native.Handle.ToString()), self.pfn_subclass, 0, 0) | ||
|
|
||
| def _remove_subclass(self): | ||
| RemoveWindowSubclass(int(self.native.Handle.ToString()), self.pfn_subclass, 0) | ||
|
|
||
| def _subclass_proc( | ||
| self, | ||
| hWnd: int, | ||
| uMsg: int, | ||
| wParam: int, | ||
| lParam: int, | ||
| uIdSubclass: int, | ||
| dwRefData: int, | ||
| ) -> LRESULT: | ||
| # Remove the window subclass in the way recommended by Raymond Chen here: | ||
| # https://devblogs.microsoft.com/oldnewthing/20031111-00/?p=41883 | ||
| if uMsg == wc.WM_NCDESTROY: | ||
| RemoveWindowSubclass(hWnd, self.pfn_subclass, uIdSubclass) | ||
|
|
||
| if uMsg == wc.WM_REFLECT_NOTIFY: | ||
| phdr = cast(lParam, POINTER(NMHDR)).contents | ||
| code = phdr.code | ||
| if code == wc.LVN_GETDISPINFOW: | ||
| disp_info = cast(lParam, POINTER(NMLVDISPINFOW)).contents | ||
| self._set_subitem_icon(disp_info.item) | ||
|
|
||
| # Call the original window procedure | ||
| return DefSubclassProc(HWND(hWnd), UINT(uMsg), WPARAM(wParam), LPARAM(lParam)) | ||
|
|
||
| def _set_subitem_icon(self, lvitem: LVITEMW): | ||
| row_index = lvitem.iItem | ||
| column_index = lvitem.iSubItem | ||
|
|
||
| _, icon_indices = self._toga_retrieve_virtual_item(row_index) | ||
|
|
||
| # Add the icon property if it doesn't exist. | ||
| if lvitem.uiMask == (wc.LVIF_TEXT | wc.LVIF_STATE): | ||
| lvitem.uiMask = wc.LVIF_TEXT | wc.LVIF_STATE | wc.LVIF_IMAGE | ||
|
|
||
| if lvitem.uiMask & wc.LVIF_IMAGE != 0 and icon_indices[column_index] > -1: | ||
| lvitem.iImage = icon_indices[column_index] | ||
|
|
||
| def add_action_events(self): | ||
| self.native.MouseDoubleClick += WeakrefCallable(self.winforms_double_click) | ||
|
|
||
|
|
@@ -80,14 +164,17 @@ def winforms_retrieve_virtual_item(self, sender, e): | |
| # Because ListView is in VirtualMode, it's necessary implement | ||
| # VirtualItemsSelectionRangeChanged event to create ListViewItem | ||
| # when it's needed | ||
| e.Item, _ = self._toga_retrieve_virtual_item(e.ItemIndex) | ||
|
|
||
| def _toga_retrieve_virtual_item(self, item_index): | ||
| if ( | ||
| self._cache | ||
| and e.ItemIndex >= self._first_item | ||
| and e.ItemIndex < self._first_item + len(self._cache) | ||
| and item_index >= self._first_item | ||
| and item_index < self._first_item + len(self._cache) | ||
| ): | ||
| e.Item = self._cache[e.ItemIndex - self._first_item] | ||
| return self._cache[item_index - self._first_item] | ||
| else: | ||
| e.Item = self._new_item(e.ItemIndex) | ||
| return self._new_item(item_index) | ||
|
|
||
| def winforms_cache_virtual_items(self, sender, e): | ||
| if ( | ||
|
|
@@ -194,28 +281,26 @@ def _resize_columns(self): | |
| def change_source(self, source): | ||
| self.update_data() | ||
|
|
||
| def _icon_index(self, row, column) -> int: | ||
| icon = column.icon(row) | ||
| return -1 if icon is None else self._image_index(icon._impl) | ||
|
|
||
| def _new_item(self, index): | ||
| item = self._data[index] | ||
| row = self._data[index] | ||
|
|
||
| missing_value = self.interface.missing_value | ||
| lvi = WinForms.ListViewItem( | ||
| [column.text(item, missing_value) for column in self._columns], | ||
| [column.text(row, missing_value) for column in self._columns], | ||
| ) | ||
| if any(column.widget(item) is not None for column in self._columns): | ||
| if any(column.widget(row) is not None for column in self._columns): | ||
| warn( | ||
| "Winforms does not support the use of widgets in cells", | ||
| stacklevel=1, | ||
| ) | ||
|
|
||
| # If the table has accessors, populate the icons for the table. | ||
| if self._columns: | ||
| # TODO: ListView only has built-in support for one icon per row. One | ||
| # possible workaround is in https://stackoverflow.com/a/46128593. | ||
| icon = self._columns[0].icon(item) | ||
| if icon is not None: | ||
| lvi.ImageIndex = self._image_index(icon._impl) | ||
| icon_indices = tuple(self._icon_index(row, column) for column in self._columns) | ||
|
|
||
| return lvi | ||
| return (lvi, icon_indices) | ||
|
|
||
| def _image_index(self, icon): | ||
| images = self.native.SmallImageList.Images | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.