Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d27e62f
Winforms Tree widget
Oliver-Leigh Mar 10, 2026
37b17a8
Added background color change functionality
Oliver-Leigh Mar 10, 2026
c0a1e3d
Added "row path" __getitem__ to StateNode
Oliver-Leigh Mar 10, 2026
b0515a5
Fixed and simplified expand/collapse functionality
Oliver-Leigh Mar 11, 2026
cffad63
Fixed selection color when not focused.
Oliver-Leigh Mar 11, 2026
4e2ee17
Fixed get_selection
Oliver-Leigh Mar 11, 2026
1670022
Added test probe for Tree widget
Oliver-Leigh Mar 12, 2026
62cf0c3
Fixed formatting
Oliver-Leigh Mar 12, 2026
f1d90c6
Fixed regex string and enabled WinForms tree tests
Oliver-Leigh Mar 12, 2026
8ed6903
Enable multi-column icon tests for WinForms
Oliver-Leigh Mar 12, 2026
2f7c3e0
Minor style change
Oliver-Leigh Mar 12, 2026
48275da
Added change note
Oliver-Leigh Mar 12, 2026
787af46
Removed types from docstring parameters
Oliver-Leigh Mar 13, 2026
a541e4e
Changed formatting for long-argument functions
Oliver-Leigh Mar 13, 2026
b3088a0
Style change and added all columns for non-leaf
Oliver-Leigh Mar 17, 2026
8db21cf
Updated testbed for previous changes
Oliver-Leigh Mar 17, 2026
55bd421
Disabled focus rectangle
Oliver-Leigh Mar 17, 2026
20d7b0c
Added image for docs
Oliver-Leigh Mar 17, 2026
4d7156f
Merge branch 'main' into winforms-tree-widget
freakboy3742 Mar 18, 2026
c6768d7
Add a widget registration for Winforms Tree.
freakboy3742 Mar 18, 2026
aed4f3f
Add a second level for the tree data.
freakboy3742 Mar 18, 2026
708afb0
Minor code format tweak.
freakboy3742 Mar 18, 2026
09b01b6
Merge remote-tracking branch 'upstream/main' into winforms-tree-widget
freakboy3742 Mar 18, 2026
1d53f14
Removed unused methods & added 4 pragma: no cover
Oliver-Leigh Mar 19, 2026
f981178
Test selection during toggle and non-visible nodes
Oliver-Leigh Mar 19, 2026
d167905
Added tests for mouse events
Oliver-Leigh Mar 19, 2026
401284c
Removed new tests for macOS and Linux
Oliver-Leigh Mar 19, 2026
8c1ce23
Implemented focused item functionality
Oliver-Leigh Mar 21, 2026
de665ef
Minor changes and added 1 pragma no cover
Oliver-Leigh Mar 21, 2026
8c2d8e5
Updated tests to cover focus rectangle code
Oliver-Leigh Mar 22, 2026
fb15fb7
Remove expand/collapse platform specific assert.s
Oliver-Leigh Mar 22, 2026
8cc2dd2
test_mouse_events as pytest.skip on non-Windows
Oliver-Leigh Mar 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4235.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Tree widget is now supported in the Windows backend.
17 changes: 7 additions & 10 deletions testbed/tests/widgets/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def source():

@pytest.fixture
async def widget(source, on_select_handler, on_activate_handler):
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
["A", "B", "C"],
data=source,
Expand All @@ -120,7 +120,7 @@ async def widget(source, on_select_handler, on_activate_handler):

@pytest.fixture
async def headerless_widget(source, on_select_handler):
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
columns=[
AccessorColumn(None, "a"),
Expand Down Expand Up @@ -152,7 +152,7 @@ async def headerless_probe(main_window, headerless_widget):
@pytest.fixture
async def multiselect_widget(source, on_select_handler):
# Although Android *has* a table implementation, it needs to be rebuilt.
skip_on_platforms("iOS", "android", "windows")
skip_on_platforms("iOS", "android")
return toga.Tree(
["A", "B", "C"],
data=source,
Expand All @@ -179,11 +179,7 @@ async def multiselect_probe(main_window, multiselect_widget):
test_cleanup = build_cleanup_test(
toga.Tree,
kwargs={"columns": ["A", "B", "C"]},
skip_platforms=(
"iOS",
"android",
"windows",
),
skip_platforms=("iOS", "android"),
)


Expand Down Expand Up @@ -913,13 +909,14 @@ async def test_cell_widget(widget, probe):
warning_check = contextlib.nullcontext()
else:
warning_check = pytest.warns(
match=".* does not support the use of widgets in cells"
match=r".* does not support the use of widgets in cells"
)

with warning_check:
widget.data = data

# Qt backend doesn't know there are widgets until the row is expanded
# Qt and Windows backends don't know there are widgets until the row is
# expanded.
await probe.expand_tree()
await probe.redraw("Tree has data with widgets")

Expand Down
2 changes: 2 additions & 0 deletions winforms/src/toga_winforms/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .widgets.table import Table
from .widgets.textinput import TextInput
from .widgets.timeinput import TimeInput
from .widgets.tree import Tree
from .widgets.webview import WebView
from .window import MainWindow, Window

Expand Down Expand Up @@ -77,6 +78,7 @@ def not_implemented(feature):
"Table",
"TextInput",
"TimeInput",
"Tree",
"WebView",
# Windows
"Window",
Expand Down
10 changes: 8 additions & 2 deletions winforms/src/toga_winforms/libs/comctl32.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from ctypes import windll
from ctypes.wintypes import BOOL, HWND, LPARAM, UINT, WPARAM
from ctypes.wintypes import BOOL, HDC, HWND, INT, LPARAM, UINT, WPARAM

from .comctl32classes import SUBCLASSPROC
from .win32 import DWORD_PTR, LRESULT, UINT_PTR
from .win32 import DWORD_PTR, HIMAGELIST, LRESULT, UINT_PTR

comctl32 = windll.comctl32

Expand All @@ -13,6 +13,12 @@
DefSubclassProc.argtypes = [HWND, UINT, WPARAM, LPARAM]


# https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-imagelist_draw
ImageList_Draw = comctl32.ImageList_Draw
ImageList_Draw.restype = BOOL
ImageList_Draw.argtypes = [HIMAGELIST, INT, HDC, INT, INT, UINT]


# https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass
RemoveWindowSubclass = comctl32.RemoveWindowSubclass
RemoveWindowSubclass.restype = BOOL
Expand Down
44 changes: 43 additions & 1 deletion winforms/src/toga_winforms/libs/comctl32classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
WINFUNCTYPE,
Structure as c_Structure,
)
from ctypes.wintypes import HWND, INT, LPARAM, LPWSTR, UINT, WPARAM
from ctypes.wintypes import (
COLORREF,
DWORD,
HDC,
HWND,
INT,
LPARAM,
LPWSTR,
RECT,
UINT,
WPARAM,
)

from .win32 import DWORD_PTR, INT_PTR, LRESULT, PUINT, UINT_PTR

Expand Down Expand Up @@ -37,6 +48,37 @@ class NMHDR(c_Structure):
]


# https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-nmcustomdraw
class NMCUSTOMDRAW(c_Structure):
_fields_ = [
("hdr", NMHDR),
("dwDrawStage", DWORD),
("hdc", HDC),
("rc", RECT),
("dwItemSpec", DWORD_PTR),
("uItemState", UINT),
("lItemlParam", LPARAM),
]


# https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-nmlvcustomdraw
class NMLVCUSTOMDRAW(c_Structure):
_fields_ = [
("nmcd", NMCUSTOMDRAW),
("clrText", COLORREF),
("clrTextBk", COLORREF),
("iSubItem", INT),
("dwItemType", DWORD),
("clrFace", COLORREF),
("iIconEffect", INT),
("iIconPhase", INT),
("iPartId", INT),
("iStateId", INT),
("rcText", RECT),
("uAlign", UINT),
]


# https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-nmlvdispinfow
class NMLVDISPINFOW(c_Structure):
_fields_ = [
Expand Down
24 changes: 24 additions & 0 deletions winforms/src/toga_winforms/libs/gdi32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from ctypes import windll
from ctypes.wintypes import BOOL, COLORREF, HDC

from .win32 import HBRUSH, HGDIOBJ

gdi32 = windll.GDI32


# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createsolidbrush
CreateSolidBrush = gdi32.CreateSolidBrush
CreateSolidBrush.restype = HBRUSH
CreateSolidBrush.argtypes = [COLORREF]


# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-deleteobject
DeleteObject = gdi32.DeleteObject
DeleteObject.restype = BOOL
DeleteObject.argtypes = [HGDIOBJ]


# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-settextcolor
SetTextColor = gdi32.SetTextColor
SetTextColor.restype = COLORREF
SetTextColor.argtypes = [HDC, COLORREF]
63 changes: 47 additions & 16 deletions winforms/src/toga_winforms/libs/user32.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from ctypes import c_void_p, windll
from ctypes.wintypes import BOOL, DWORD, HMONITOR, HWND, LPARAM, LPRECT, UINT, WPARAM
from ctypes.wintypes import (
BOOL,
DWORD,
HDC,
HMONITOR,
HWND,
INT,
LPARAM,
LPCWSTR,
LPRECT,
UINT,
WPARAM,
)

from System import Environment

from .win32 import LRESULT
from .win32 import HBRUSH, LRESULT, RECT_PTR

user32 = windll.user32

Expand All @@ -12,6 +24,39 @@
DPI_AWARENESS_CONTEXT_UNAWARE = -1
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtextw
DrawTextW = user32.DrawTextW
DrawTextW.restype = INT
DrawTextW.argtypes = [HDC, LPCWSTR, INT, LPRECT, UINT]


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-fillrect
FillRect = user32.FillRect
FillRect.restype = INT
FillRect.argtypes = [HDC, RECT_PTR, HBRUSH]


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor
GetSysColor = user32.GetSysColor
GetSysColor.restype = DWORD
GetSysColor.argtypes = [INT]


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromrect
MONITOR_DEFAULTTONEAREST = 2

MonitorFromRect = user32.MonitorFromRect
MonitorFromRect.restype = HMONITOR
MonitorFromRect.argtypes = [LPRECT, DWORD]


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagew
SendMessageW = user32.SendMessageW
SendMessageW.restype = LRESULT
SendMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM]


# https://www.lifewire.com/windows-version-numbers-2625171
win_version = Environment.OSVersion.Version
if (win_version.Major, win_version.Minor, win_version.Build) >= (10, 0, 15063):
Expand All @@ -29,17 +74,3 @@
"We recommend you upgrade to at least Windows 10 version 1703."
)
SetProcessDpiAwarenessContext = SetThreadDpiAwarenessContext = None


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromrect
MONITOR_DEFAULTTONEAREST = 2

MonitorFromRect = user32.MonitorFromRect
MonitorFromRect.restype = HMONITOR
MonitorFromRect.argtypes = [LPRECT, DWORD]


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagew
SendMessageW = user32.SendMessageW
SendMessageW.restype = LRESULT
SendMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM]
8 changes: 6 additions & 2 deletions winforms/src/toga_winforms/libs/win32.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from ctypes import c_size_t
from ctypes.wintypes import LPARAM
from ctypes import POINTER, c_size_t
from ctypes.wintypes import HWND, LPARAM, RECT

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
RECT_PTR = POINTER(RECT)
HBRUSH = HWND
HIMAGELIST = HWND
HGDIOBJ = HWND
41 changes: 40 additions & 1 deletion winforms/src/toga_winforms/libs/windowconstants.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
# Window constants

# Custom Draw Draw Stage
CDDS_PREPAINT = 0x00000001
CDDS_ITEMPREPAINT = 0x00010000 | 0x00000001
CDDS_SUBITEM = 0x00020000

# Custom Draw Response Flag
CDRF_NEWFONT = 0x00000002
CDRF_NOTIFYITEMDRAW = 0x00000020
CDRF_NOTIFYSUBITEMDRAW = CDRF_NOTIFYITEMDRAW
CDRF_SKIPDEFAULT = 0x00000004

# Color
COLOR_HIGHLIGHT = 13
COLOR_HIGHLIGHTTEXT = 14
COLOR_HOTLIGHT = 26
COLOR_WINDOW = 5
COLOR_BTNFACE = 15
COLOR_BTNTEXT = 18

# Draw Text
DT_CALCRECT = 0x00000400
DT_HCENTER = 0x00000001
DT_NOCLIP = 0x00000100
DT_SINGLELINE = 0x00000020
DT_VCENTER = 0x00000004
DT_WORD_ELLIPSIS = 0x40000

# Edit Messages
EM_SETCUEBANNER = 0x1501

# List-View Item Format
# Image List Draw
ILD_NORMAL = 0x0000
ILD_SELECTED = 0x0004

# List-View Item Flag
LVIF_TEXT = 0x0001
LVIF_IMAGE = 0x0002
LVIF_STATE = 0x0008

# List-View Item State
LVIS_SELECTED = 0x0002

# List-View Management
LVM_GETEXTENDEDLISTVIEWSTYLE = 0x1037
LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1036
Expand All @@ -18,6 +52,11 @@
# List-View Styles (Extended)
LVS_EX_SUBITEMIMAGES = 0x2

# Notification Message
NM_CUSTOMDRAW = 0xFFFFFFF4

# Window Message
WM_GETFONT = 0x0031
WM_NCDESTROY = 0x0082
WM_NOTIFY = 0x004E
WM_REFLECT_NOTIFY = 0x204E
7 changes: 7 additions & 0 deletions winforms/src/toga_winforms/widgets/detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ def __getitem__(self, index):


class DetailedList(Table):
#################################################################################
# The following methods are overridden from Table.
#################################################################################

@property
def _show_headings(self):
return False
Expand All @@ -48,6 +51,10 @@ def add_action_events(self):
# DetailedList doesn't have an on_activate_handler.
pass

#################################################################################
# The following methods and class variable are not from Table.
#################################################################################

def set_primary_action_enabled(self, enabled):
self.primary_action_enabled = enabled

Expand Down
Loading
Loading