Skip to content

Commit f8a1415

Browse files
committed
improve: apply DPI scaling to radio buttons and checkboxes
1 parent 1d22df8 commit f8a1415

File tree

4 files changed

+358
-2
lines changed

4 files changed

+358
-2
lines changed

examples/demo_timepicker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(self):
2626
self.root.title("tkface TimePicker Demo")
2727
# Hide window initially until setup is complete
2828
self.root.withdraw()
29-
# Enable DPI-aware geometry (automatically adjusts window size)
29+
# Enable DPI-aware geometry (automatically adjusts window size and configures ttk widgets)
3030
tkface.win.dpi(self.root)
3131
# Force update to ensure DPI scaling is applied before setting geometry
3232
self.root.update_idletasks()

tests/test_dpi.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
Tests for tkface.win.dpi module.
33
4-
This module tests the DPI management functionality for Windows applications.
4+
This module tests the DPI management functionality for Windows applications,
5+
including automatic ttk widget configuration and tk widget patching.
56
"""
67

78
import contextlib
@@ -12,6 +13,7 @@
1213
from unittest.mock import MagicMock, call, patch
1314

1415
import pytest
16+
from tkinter import ttk
1517

1618

1719
# Common mock fixtures for DPI testing
@@ -3105,3 +3107,160 @@ def test_scale_icon_between_1_0_and_1_25(self):
31053107
result = scale_icon('error', parent)
31063108
assert result == 'scaled_error_large'
31073109

3110+
3111+
class TestTTKWidgetDPI:
3112+
"""Test cases for automatic ttk widget DPI configuration."""
3113+
3114+
def test_configure_ttk_widgets_for_dpi_non_windows(self):
3115+
"""Test configure_ttk_widgets_for_dpi on non-Windows platform."""
3116+
with patch('tkface.win.dpi.is_windows', return_value=False):
3117+
from tkface.win.dpi import configure_ttk_widgets_for_dpi
3118+
# Should return early without error
3119+
configure_ttk_widgets_for_dpi(None)
3120+
3121+
def test_configure_ttk_widgets_for_dpi_no_root(self):
3122+
"""Test configure_ttk_widgets_for_dpi with None root."""
3123+
with patch('tkface.win.dpi.is_windows', return_value=True):
3124+
from tkface.win.dpi import configure_ttk_widgets_for_dpi
3125+
# Should return early without error
3126+
configure_ttk_widgets_for_dpi(None)
3127+
3128+
def test_configure_ttk_widgets_for_dpi_low_scaling(self):
3129+
"""Test configure_ttk_widgets_for_dpi with scaling factor <= 1.0."""
3130+
mock_root = MagicMock()
3131+
mock_root.DPI_scaling = 1.0
3132+
3133+
with patch('tkface.win.dpi.is_windows', return_value=True):
3134+
from tkface.win.dpi import configure_ttk_widgets_for_dpi
3135+
# Should return early without configuring styles
3136+
configure_ttk_widgets_for_dpi(mock_root)
3137+
3138+
def test_configure_ttk_widgets_for_dpi_success(self):
3139+
"""Test successful ttk widget configuration."""
3140+
mock_root = MagicMock()
3141+
mock_root.DPI_scaling = 2.0
3142+
3143+
mock_style = MagicMock()
3144+
3145+
with patch('tkface.win.dpi.is_windows', return_value=True), \
3146+
patch('tkinter.ttk.Style', return_value=mock_style):
3147+
from tkface.win.dpi import configure_ttk_widgets_for_dpi
3148+
configure_ttk_widgets_for_dpi(mock_root)
3149+
3150+
# Should configure both Checkbutton and Radiobutton styles
3151+
assert mock_style.configure.call_count == 2
3152+
calls = mock_style.configure.call_args_list
3153+
assert calls[0][0][0] == "TCheckbutton" # First call for Checkbutton
3154+
assert calls[1][0][0] == "TRadiobutton" # Second call for Radiobutton
3155+
3156+
def test_configure_ttk_widgets_for_dpi_style_error(self):
3157+
"""Test configure_ttk_widgets_for_dpi with style configuration errors."""
3158+
mock_root = MagicMock()
3159+
mock_root.DPI_scaling = 2.0
3160+
3161+
mock_style = MagicMock()
3162+
mock_style.configure.side_effect = Exception("Style error")
3163+
3164+
with patch('tkface.win.dpi.is_windows', return_value=True), \
3165+
patch('tkinter.ttk.Style', return_value=mock_style):
3166+
from tkface.win.dpi import configure_ttk_widgets_for_dpi
3167+
# Should not raise exception, should handle errors gracefully
3168+
configure_ttk_widgets_for_dpi(mock_root)
3169+
3170+
3171+
class TestAutoPatchTKWidgets:
3172+
"""Test cases for automatic tk widget to ttk patching."""
3173+
3174+
def test_auto_patch_tk_widgets_non_windows(self):
3175+
"""Test auto-patch on non-Windows platform."""
3176+
with patch('tkface.win.dpi.is_windows', return_value=False):
3177+
from tkface.win.dpi import DPIManager
3178+
manager = DPIManager()
3179+
# Should return early without patching
3180+
manager._auto_patch_tk_widgets_to_ttk()
3181+
3182+
def test_auto_patch_tk_widgets_already_patched(self):
3183+
"""Test auto-patch when widgets are already patched."""
3184+
# Mark widgets as already patched
3185+
tk.Checkbutton._tkface_patched_to_ttk = True
3186+
tk.Radiobutton._tkface_patched_to_ttk = True
3187+
3188+
with patch('tkface.win.dpi.is_windows', return_value=True):
3189+
from tkface.win.dpi import DPIManager
3190+
manager = DPIManager()
3191+
# Should return early without double-patching
3192+
manager._auto_patch_tk_widgets_to_ttk()
3193+
3194+
def test_auto_patch_tk_widgets_success(self):
3195+
"""Test successful auto-patching of tk widgets."""
3196+
# Remove patch markers if they exist
3197+
if hasattr(tk.Checkbutton, '_tkface_patched_to_ttk'):
3198+
delattr(tk.Checkbutton, '_tkface_patched_to_ttk')
3199+
if hasattr(tk.Radiobutton, '_tkface_patched_to_ttk'):
3200+
delattr(tk.Radiobutton, '_tkface_patched_to_ttk')
3201+
3202+
original_checkbutton_init = tk.Checkbutton.__init__
3203+
original_radiobutton_init = tk.Radiobutton.__init__
3204+
3205+
try:
3206+
with patch('tkface.win.dpi.is_windows', return_value=True):
3207+
from tkface.win.dpi import DPIManager
3208+
manager = DPIManager()
3209+
manager._auto_patch_tk_widgets_to_ttk()
3210+
3211+
# Check that widgets are marked as patched
3212+
assert hasattr(tk.Checkbutton, '_tkface_patched_to_ttk')
3213+
assert hasattr(tk.Radiobutton, '_tkface_patched_to_ttk')
3214+
3215+
# Check that constructors were replaced
3216+
assert tk.Checkbutton.__init__ != original_checkbutton_init
3217+
assert tk.Radiobutton.__init__ != original_radiobutton_init
3218+
3219+
finally:
3220+
# Restore original constructors
3221+
tk.Checkbutton.__init__ = original_checkbutton_init
3222+
tk.Radiobutton.__init__ = original_radiobutton_init
3223+
if hasattr(tk.Checkbutton, '_tkface_patched_to_ttk'):
3224+
delattr(tk.Checkbutton, '_tkface_patched_to_ttk')
3225+
if hasattr(tk.Radiobutton, '_tkface_patched_to_ttk'):
3226+
delattr(tk.Radiobutton, '_tkface_patched_to_ttk')
3227+
3228+
def test_auto_patch_tk_widgets_exception_handling(self):
3229+
"""Test auto-patch exception handling."""
3230+
with patch('tkface.win.dpi.is_windows', return_value=True), \
3231+
patch.object(tk.Checkbutton, '__init__', side_effect=Exception("Patch error")):
3232+
from tkface.win.dpi import DPIManager
3233+
manager = DPIManager()
3234+
# Should not raise exception, should handle errors gracefully
3235+
manager._auto_patch_tk_widgets_to_ttk()
3236+
3237+
3238+
class TestDPIIntegration:
3239+
"""Test cases for integrated DPI functionality."""
3240+
3241+
def test_dpi_function_includes_ttk_configuration(self):
3242+
"""Test that dpi() function includes ttk widget configuration."""
3243+
mock_root = MagicMock()
3244+
mock_root.winfo_id.return_value = 12345
3245+
mock_root.DPI_scaling = 2.0
3246+
3247+
with patch('tkface.win.dpi.is_windows', return_value=True), \
3248+
patch('tkface.win.dpi.DPIManager._get_hwnd_dpi', return_value=(192, 192, 2.0)), \
3249+
patch('tkface.win.dpi.DPIManager._enable_dpi_awareness', return_value={"shcore": True}), \
3250+
patch('tkface.win.dpi.DPIManager._apply_shcore_dpi_scaling'), \
3251+
patch('tkface.win.dpi.DPIManager._fix_scaling'), \
3252+
patch('tkface.win.dpi.DPIManager._apply_scaling_methods'), \
3253+
patch('tkface.win.dpi.DPIManager._configure_ttk_widgets_for_dpi_internal') as mock_ttk_config, \
3254+
patch('tkface.win.dpi.DPIManager._auto_patch_tk_widgets_to_ttk') as mock_auto_patch:
3255+
3256+
from tkface.win.dpi import dpi
3257+
result = dpi(mock_root)
3258+
3259+
# Should call both ttk configuration and auto-patch
3260+
mock_ttk_config.assert_called_once_with(mock_root)
3261+
mock_auto_patch.assert_called_once()
3262+
3263+
# Should return successful result
3264+
assert result["enabled"] is True
3265+
assert result["dpi_awareness_set"] is True
3266+

tkface/win/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .dpi import (
2121
add_scalable_property,
2222
calculate_dpi_sizes,
23+
configure_ttk_widgets_for_dpi,
2324
disable_auto_dpi_scaling,
2425
dpi,
2526
enable_auto_dpi_scaling,
@@ -31,6 +32,7 @@
3132
get_scaling_factor,
3233
is_auto_dpi_scaling_enabled,
3334
logical_to_physical,
35+
patch_tk_widgets_to_ttk,
3436
physical_to_logical,
3537
remove_scalable_property,
3638
scale_font_size,
@@ -52,6 +54,7 @@
5254
"get_actual_window_size",
5355
"enable_dpi_awareness",
5456
"calculate_dpi_sizes",
57+
"configure_ttk_widgets_for_dpi",
5558
"scale_icon",
5659
"scale_font_size",
5760
"get_effective_dpi",
@@ -65,6 +68,7 @@
6568
"get_scalable_properties",
6669
"add_scalable_property",
6770
"remove_scalable_property",
71+
"patch_tk_widgets_to_ttk",
6872
"configure_button_for_windows",
6973
"get_button_label_with_shortcut",
7074
"FlatButton",

0 commit comments

Comments
 (0)