CTkDataTable 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/ctk_data_table.py +258 -55
  2. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/examples/basic_table.py +2 -1
  3. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_column.py +1 -1
  4. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_renderer.py +74 -53
  5. ctkdatatable-0.2.0/CTkDataTable.egg-info/PKG-INFO +384 -0
  6. ctkdatatable-0.2.0/PKG-INFO +384 -0
  7. ctkdatatable-0.2.0/README.md +352 -0
  8. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/pyproject.toml +5 -5
  9. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_ctk_data_table_gui.py +67 -11
  10. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_renderer.py +3 -2
  11. ctkdatatable-0.1.0/CTkDataTable.egg-info/PKG-INFO +0 -681
  12. ctkdatatable-0.1.0/PKG-INFO +0 -681
  13. ctkdatatable-0.1.0/README.md +0 -649
  14. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/__init__.py +0 -0
  15. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/_utils.py +0 -0
  16. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/examples/__init__.py +0 -0
  17. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/py.typed +0 -0
  18. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_events.py +0 -0
  19. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_model.py +0 -0
  20. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable/table_style.py +0 -0
  21. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/SOURCES.txt +0 -0
  22. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/dependency_links.txt +0 -0
  23. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/requires.txt +0 -0
  24. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/CTkDataTable.egg-info/top_level.txt +0 -0
  25. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/LICENSE +0 -0
  26. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/setup.cfg +0 -0
  27. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_column.py +0 -0
  28. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_model.py +0 -0
  29. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_table_style.py +0 -0
  30. {ctkdatatable-0.1.0 → ctkdatatable-0.2.0}/tests/test_utils.py +0 -0
@@ -1,4 +1,6 @@
1
1
  """A virtualized Canvas-rendered data table for CustomTkinter."""
2
+ # By Harry Gomm
3
+ # And OpenAI GPT 5.5
2
4
 
3
5
  from __future__ import annotations
4
6
 
@@ -36,6 +38,7 @@ SummaryDefinition = str | Callable[[list[RowData]], Any]
36
38
  AsyncFetchCallback = Callable[[], Iterable[Any]]
37
39
  AsyncSuccessCallback = Callable[[list[dict[str, Any]]], None]
38
40
  AsyncErrorCallback = Callable[[BaseException], None]
41
+ ColumnWidthMode = Literal["fixed", "fill"]
39
42
 
40
43
 
41
44
  class CTkDataTable(ctk.CTkFrame):
@@ -56,6 +59,7 @@ class CTkDataTable(ctk.CTkFrame):
56
59
  font: Any | None = None,
57
60
  header_font: Any | None = None,
58
61
  horizontal_scroll: bool = False,
62
+ column_width_mode: ColumnWidthMode = "fixed",
59
63
  multi_select: bool = False,
60
64
  searchable: bool = False,
61
65
  search_delay_ms: int = 0,
@@ -116,19 +120,23 @@ class CTkDataTable(ctk.CTkFrame):
116
120
  raise ValueError("search_delay_ms must not be negative.")
117
121
  if min_column_width < 24:
118
122
  raise ValueError("min_column_width must be at least 24 pixels.")
123
+ if column_width_mode not in {"fixed", "fill"}:
124
+ raise ValueError("column_width_mode must be 'fixed' or 'fill'.")
119
125
  if not enable_style_hooks and (row_style is not None or cell_style is not None):
120
126
  raise ValueError("Set enable_style_hooks=True before passing row_style or cell_style callbacks.")
121
127
 
122
128
  self._columns = normalize_columns(columns)
123
129
  self._model = TableModel(self._columns)
124
130
  self._visible_columns_cache: list[TableColumn] = []
131
+ self._layout_columns_cache: list[TableColumn] = []
125
132
  self._column_edges_cache: tuple[float, ...] = ()
126
133
  self._column_edge_columns_cache: tuple[TableColumn, ...] = ()
127
- self._total_table_width_cache = 0
134
+ self._total_table_width_cache = 0.0
128
135
  self._row_height = row_height
129
136
  self._header_height = header_height
130
137
  self._footer_height = footer_height
131
138
  self._horizontal_scroll_enabled = horizontal_scroll
139
+ self._column_width_mode = column_width_mode
132
140
  self._multi_select = multi_select
133
141
  self._searchable = searchable
134
142
  self._search_delay_ms = search_delay_ms
@@ -176,13 +184,16 @@ class CTkDataTable(ctk.CTkFrame):
176
184
  self._y_offset = 0.0
177
185
  self._x_offset = 0.0
178
186
 
179
- self._font = font if font is not None else self._make_font(size=13)
187
+ self._font_source = font if font is not None else self._make_font(size=13)
180
188
  if header_font is not None:
181
- self._header_font = header_font
189
+ self._header_font_source = header_font
182
190
  elif font is not None:
183
- self._header_font = font
191
+ self._header_font_source = font
184
192
  else:
185
- self._header_font = self._make_font(size=13, weight="bold")
193
+ self._header_font_source = self._make_font(size=13, weight="bold")
194
+ self._font = self._font_source
195
+ self._header_font = self._header_font_source
196
+ self._font_callback_sources: list[Any] = []
186
197
  self._refresh_column_cache()
187
198
 
188
199
  self.grid_columnconfigure(0, weight=1)
@@ -200,7 +211,14 @@ class CTkDataTable(ctk.CTkFrame):
200
211
  else:
201
212
  self.grid_rowconfigure(0, weight=1)
202
213
 
203
- self._table_canvas = tk.Canvas(self, bd=0, highlightthickness=0, takefocus=True)
214
+ self._table_canvas = tk.Canvas(
215
+ self,
216
+ bd=0,
217
+ highlightthickness=0,
218
+ takefocus=True,
219
+ width=self._scale_dimension(self._desired_width),
220
+ height=self._scale_dimension(self._desired_height),
221
+ )
204
222
  self._table_canvas.grid(row=canvas_row, column=0, sticky="nsew")
205
223
 
206
224
  self._vertical_scrollbar = ctk.CTkScrollbar(
@@ -219,6 +237,8 @@ class CTkDataTable(ctk.CTkFrame):
219
237
  )
220
238
  self._horizontal_scrollbar.grid(row=canvas_row + 1, column=0, sticky="ew", padx=2, pady=(0, 2))
221
239
 
240
+ self._refresh_scaled_fonts()
241
+ self._register_font_callbacks()
222
242
  self._renderer = TableRenderer(self._table_canvas, self._font, self._header_font, self._resolve_color)
223
243
  self._apply_renderer_style()
224
244
  self._apply_layout_insets()
@@ -264,9 +284,27 @@ class CTkDataTable(ctk.CTkFrame):
264
284
 
265
285
  return self._model.require_column(column_key).width
266
286
 
287
+ def set_column_width_mode(self, mode: ColumnWidthMode) -> None:
288
+ """Set how visible columns are laid out across the table width."""
289
+
290
+ if mode not in {"fixed", "fill"}:
291
+ raise ValueError("column_width_mode must be 'fixed' or 'fill'.")
292
+ if mode == self._column_width_mode:
293
+ return
294
+ self._column_width_mode = mode
295
+ self._refresh_column_cache()
296
+ self._clamp_offsets()
297
+ self._redraw(full=True)
298
+
299
+ def get_column_width_mode(self) -> ColumnWidthMode:
300
+ """Return the current column width layout mode."""
301
+
302
+ return self._column_width_mode
303
+
267
304
  def refresh(self) -> None:
268
305
  """Refresh the current rendered view without changing data or selection."""
269
306
 
307
+ self._refresh_column_cache()
270
308
  self._clamp_offsets()
271
309
  self._redraw(full=True)
272
310
 
@@ -526,9 +564,90 @@ class CTkDataTable(ctk.CTkFrame):
526
564
  font.configure(size=size, weight=weight)
527
565
  return font
528
566
 
567
+ def _register_font_callbacks(self) -> None:
568
+ for font in (self._font_source, self._header_font_source):
569
+ already_registered = any(font is registered for registered in self._font_callback_sources)
570
+ if isinstance(font, ctk.CTkFont) and not already_registered:
571
+ font.add_size_configure_callback(self._handle_font_configure)
572
+ self._font_callback_sources.append(font)
573
+
574
+ def _remove_font_callbacks(self) -> None:
575
+ for font in list(self._font_callback_sources):
576
+ with suppress(ValueError):
577
+ font.remove_size_configure_callback(self._handle_font_configure)
578
+ self._font_callback_sources.clear()
579
+
580
+ def _handle_font_configure(self) -> None:
581
+ self._refresh_scaled_fonts()
582
+ if hasattr(self, "_renderer"):
583
+ self._renderer.configure_fonts(self._font, self._header_font)
584
+ self._redraw(full=True)
585
+
586
+ def _refresh_scaled_fonts(self) -> None:
587
+ if not hasattr(self, "_table_canvas"):
588
+ return
589
+ self._font = self._scaled_font(self._font_source)
590
+ self._header_font = self._scaled_font(self._header_font_source)
591
+ if hasattr(self, "_renderer"):
592
+ self._renderer.configure_fonts(self._font, self._header_font)
593
+
594
+ def _scaled_font(self, font: Any) -> Any:
595
+ if isinstance(font, (ctk.CTkFont, tuple)):
596
+ try:
597
+ return tkfont.Font(root=self._table_canvas, font=self._apply_font_scaling(font))
598
+ except Exception:
599
+ return font
600
+ if isinstance(font, tkfont.Font):
601
+ try:
602
+ options = font.actual()
603
+ size = int(font.cget("size"))
604
+ scaled_size = max(1, round(abs(size) * self._widget_scale()))
605
+ options["size"] = -scaled_size if size < 0 else scaled_size
606
+ return tkfont.Font(root=self._table_canvas, **options)
607
+ except Exception:
608
+ return font
609
+ return font
610
+
611
+ def _widget_scale(self) -> float:
612
+ try:
613
+ return max(0.01, float(self._get_widget_scaling()))
614
+ except Exception:
615
+ return 1.0
616
+
617
+ def _scale_dimension(self, value: float) -> float:
618
+ try:
619
+ return float(self._apply_widget_scaling(value))
620
+ except Exception:
621
+ return float(value)
622
+
623
+ def _unscale_dimension(self, value: float) -> float:
624
+ try:
625
+ return float(self._reverse_widget_scaling(value))
626
+ except Exception:
627
+ return float(value)
628
+
629
+ def _scaled_row_height(self) -> float:
630
+ return self._scale_dimension(self._row_height)
631
+
632
+ def _scaled_header_height(self) -> float:
633
+ return self._scale_dimension(self._header_height)
634
+
635
+ def _scaled_footer_height(self) -> float:
636
+ return self._scale_dimension(self._footer_height)
637
+
638
+ def _scaled_resize_hit_width(self) -> float:
639
+ return max(self._scale_dimension(self._resize_hit_width), 1.0)
640
+
641
+ def _scaled_table_corner_radius(self) -> float:
642
+ return self._scale_dimension(self._table_corner_radius())
643
+
644
+ def _scaled_table_border_width(self) -> float:
645
+ return self._scale_dimension(self._table_border_width())
646
+
529
647
  def _handle_configure(self, event: tk.Event[Any]) -> None:
530
648
  self._canvas_width = max(1, int(event.width))
531
649
  self._canvas_height = max(1, int(event.height))
650
+ self._refresh_column_cache()
532
651
  self._clamp_offsets()
533
652
  self._redraw(full=True)
534
653
 
@@ -540,7 +659,8 @@ class CTkDataTable(ctk.CTkFrame):
540
659
  elif args[0] == "scroll" and len(args) >= 3:
541
660
  amount = int(args[1])
542
661
  unit = args[2]
543
- step = self._row_height if unit == "units" else max(self._row_height, self._body_height())
662
+ row_height = self._scaled_row_height()
663
+ step = row_height if unit == "units" else max(row_height, self._body_height())
544
664
  self._set_y_offset(self._y_offset + amount * step)
545
665
 
546
666
  def _handle_horizontal_scrollbar(self, *args: str) -> None:
@@ -551,7 +671,7 @@ class CTkDataTable(ctk.CTkFrame):
551
671
  elif args[0] == "scroll" and len(args) >= 3:
552
672
  amount = int(args[1])
553
673
  unit = args[2]
554
- step = 40 if unit == "units" else max(80, self._canvas_width)
674
+ step = self._scale_dimension(40) if unit == "units" else max(self._scale_dimension(80), self._canvas_width)
555
675
  self._set_x_offset(self._x_offset + amount * step)
556
676
 
557
677
  def _handle_mousewheel(self, event: tk.Event[Any]) -> str:
@@ -566,17 +686,18 @@ class CTkDataTable(ctk.CTkFrame):
566
686
  units = -1 if delta > 0 else 1
567
687
 
568
688
  if self._horizontal_scroll_enabled and getattr(event, "state", 0) & self._SHIFT_MASK:
569
- self._set_x_offset(self._x_offset + units * 40)
689
+ self._set_x_offset(self._x_offset + units * self._scale_dimension(40))
570
690
  else:
571
- self._set_y_offset(self._y_offset + units * self._row_height)
691
+ self._set_y_offset(self._y_offset + units * self._scaled_row_height())
572
692
  return "break"
573
693
 
574
694
  def _handle_click(self, event: tk.Event[Any]) -> str | None:
575
695
  self._table_canvas.focus_set()
576
- if event.y < self._header_height:
696
+ if event.y < self._scaled_header_height():
577
697
  resize_column = self._resize_column_from_x(event.x)
578
698
  if resize_column is not None:
579
- self._resize_state = (resize_column.key, float(event.x), resize_column.width)
699
+ start_width = self._model.require_column(resize_column.key).width
700
+ self._resize_state = (resize_column.key, float(event.x), start_width)
580
701
  return "break"
581
702
  column = self._column_from_x(event.x)
582
703
  if column is not None and column.sortable:
@@ -639,7 +760,8 @@ class CTkDataTable(ctk.CTkFrame):
639
760
  if self._resize_state is None:
640
761
  return None
641
762
  column_key, start_x, start_width = self._resize_state
642
- width = max(self._min_column_width, round(start_width + float(event.x) - start_x))
763
+ delta = self._unscale_dimension(float(event.x) - start_x)
764
+ width = max(self._min_column_width, round(start_width + delta))
643
765
  self._set_column_width(column_key, width)
644
766
  return "break"
645
767
 
@@ -676,7 +798,7 @@ class CTkDataTable(ctk.CTkFrame):
676
798
  return lambda: self._invoke_context_action(row_event, action_key)
677
799
 
678
800
  def _handle_double_click(self, event: tk.Event[Any]) -> str | None:
679
- if event.y < self._header_height or self._loading:
801
+ if event.y < self._scaled_header_height() or self._loading:
680
802
  return "break"
681
803
  action_region = self._hit_action(event.x, event.y)
682
804
  if action_region is not None and action_region.kind == "checkbox":
@@ -700,7 +822,7 @@ class CTkDataTable(ctk.CTkFrame):
700
822
  keysym = getattr(event, "keysym", "")
701
823
  focused_view_index = self._model.focused_view_index()
702
824
 
703
- page_size = max(1, self._body_height() // self._row_height)
825
+ page_size = max(1, int(self._body_height() // self._scaled_row_height()))
704
826
  last_index = len(self._model.view_indices) - 1
705
827
  target_index: int | None = None
706
828
 
@@ -781,7 +903,7 @@ class CTkDataTable(ctk.CTkFrame):
781
903
  def _handle_motion(self, event: tk.Event[Any]) -> None:
782
904
  if self._resize_state is not None:
783
905
  return
784
- if event.y < self._header_height:
906
+ if event.y < self._scaled_header_height():
785
907
  self._update_header_cursor(self._resize_column_from_x(event.x))
786
908
  else:
787
909
  action_region = None if self._loading else self._hit_action(event.x, event.y)
@@ -841,7 +963,7 @@ class CTkDataTable(ctk.CTkFrame):
841
963
  for index in {edge_index - 1, edge_index}:
842
964
  if (
843
965
  0 <= index < len(self._column_edges_cache)
844
- and abs(content_x - self._column_edges_cache[index]) <= self._resize_hit_width
966
+ and abs(content_x - self._column_edges_cache[index]) <= self._scaled_resize_hit_width()
845
967
  ):
846
968
  return self._column_edge_columns_cache[index]
847
969
  return None
@@ -1086,7 +1208,7 @@ class CTkDataTable(ctk.CTkFrame):
1086
1208
  self._renderer.draw_surface(
1087
1209
  canvas_width=self._canvas_width,
1088
1210
  canvas_height=self._canvas_height,
1089
- radius=self._table_corner_radius(),
1211
+ radius=self._scaled_table_corner_radius(),
1090
1212
  colors=colors,
1091
1213
  )
1092
1214
 
@@ -1094,8 +1216,8 @@ class CTkDataTable(ctk.CTkFrame):
1094
1216
  self._renderer.draw_chrome(
1095
1217
  canvas_width=self._canvas_width,
1096
1218
  canvas_height=self._canvas_height,
1097
- radius=self._table_corner_radius(),
1098
- border_width=self._table_border_width(),
1219
+ radius=self._scaled_table_corner_radius(),
1220
+ border_width=self._scaled_table_border_width(),
1099
1221
  bottom_cap_height=self._bottom_cap_height(),
1100
1222
  colors=colors,
1101
1223
  )
@@ -1107,8 +1229,8 @@ class CTkDataTable(ctk.CTkFrame):
1107
1229
  self._visible_columns(),
1108
1230
  x_offset=self._x_offset,
1109
1231
  canvas_width=self._canvas_width,
1110
- header_height=self._header_height,
1111
- radius=self._table_corner_radius(),
1232
+ header_height=self._scaled_header_height(),
1233
+ radius=self._scaled_table_corner_radius(),
1112
1234
  sort_key=sort_key,
1113
1235
  sort_ascending=sort_ascending,
1114
1236
  filtered_column_keys=set(self._model.column_filters),
@@ -1125,8 +1247,8 @@ class CTkDataTable(ctk.CTkFrame):
1125
1247
  x_offset=self._x_offset,
1126
1248
  canvas_width=self._canvas_width,
1127
1249
  footer_top=self._footer_top(),
1128
- footer_height=self._footer_height,
1129
- radius=self._table_corner_radius(),
1250
+ footer_height=self._scaled_footer_height(),
1251
+ radius=self._scaled_table_corner_radius(),
1130
1252
  colors=colors,
1131
1253
  )
1132
1254
  self._table_canvas.tag_raise("footer")
@@ -1174,7 +1296,7 @@ class CTkDataTable(ctk.CTkFrame):
1174
1296
  row,
1175
1297
  self._visible_columns(),
1176
1298
  y=self._row_y(row_index),
1177
- row_height=self._row_height,
1299
+ row_height=self._scaled_row_height(),
1178
1300
  x_offset=self._x_offset,
1179
1301
  canvas_width=self._canvas_width,
1180
1302
  selected=source_index in self._model.selected_source_indices,
@@ -1197,7 +1319,7 @@ class CTkDataTable(ctk.CTkFrame):
1197
1319
  ):
1198
1320
  message = "No matching records"
1199
1321
 
1200
- body_top = self._header_height
1322
+ body_top = self._scaled_header_height()
1201
1323
  body_height = self._body_height()
1202
1324
  self._table_canvas.create_rectangle(
1203
1325
  0,
@@ -1210,12 +1332,14 @@ class CTkDataTable(ctk.CTkFrame):
1210
1332
  )
1211
1333
  center_y = body_top + body_height / 2
1212
1334
  if self._loading:
1213
- indicator_width = min(160, max(80, self._canvas_width * 0.3))
1335
+ indicator_width = min(self._scale_dimension(160), max(self._scale_dimension(80), self._canvas_width * 0.3))
1336
+ indicator_top = center_y + self._scale_dimension(20)
1337
+ indicator_height = self._scale_dimension(4)
1214
1338
  self._table_canvas.create_rectangle(
1215
1339
  (self._canvas_width - indicator_width) / 2,
1216
- center_y + 20,
1340
+ indicator_top,
1217
1341
  (self._canvas_width + indicator_width) / 2,
1218
- center_y + 24,
1342
+ indicator_top + indicator_height,
1219
1343
  fill=colors["loading_indicator"],
1220
1344
  outline="",
1221
1345
  tags=("state", "loading_indicator"),
@@ -1268,16 +1392,18 @@ class CTkDataTable(ctk.CTkFrame):
1268
1392
  body_height = self._body_height()
1269
1393
  if body_height <= 0 or not self._model.view_indices:
1270
1394
  return range(0)
1271
- start = max(0, int(self._y_offset // self._row_height))
1272
- end = min(len(self._model.view_indices), int((self._y_offset + body_height) // self._row_height))
1395
+ row_height = self._scaled_row_height()
1396
+ start = max(0, int(self._y_offset // row_height))
1397
+ end = min(len(self._model.view_indices), int((self._y_offset + body_height) // row_height))
1273
1398
  return range(start, end)
1274
1399
 
1275
1400
  def _row_y(self, row_index: int) -> float:
1276
- return self._header_height + row_index * self._row_height - self._y_offset
1401
+ return self._scaled_header_height() + row_index * self._scaled_row_height() - self._y_offset
1277
1402
 
1278
1403
  def _scroll_view_index_into_view(self, view_index: int) -> None:
1279
- row_top = view_index * self._row_height
1280
- row_bottom = row_top + self._row_height
1404
+ row_height = self._scaled_row_height()
1405
+ row_top = view_index * row_height
1406
+ row_bottom = row_top + row_height
1281
1407
  viewport_top = self._y_offset
1282
1408
  viewport_bottom = self._y_offset + self._body_height()
1283
1409
 
@@ -1287,10 +1413,12 @@ class CTkDataTable(ctk.CTkFrame):
1287
1413
  self._set_y_offset(float(row_bottom - self._body_height()))
1288
1414
 
1289
1415
  def _row_index_from_y(self, y: float) -> int | None:
1290
- if y < self._header_height or y > self._footer_top():
1416
+ header_height = self._scaled_header_height()
1417
+ row_height = self._scaled_row_height()
1418
+ if y < header_height or y > self._footer_top():
1291
1419
  return None
1292
- row_index = int((y - self._header_height + self._y_offset) // self._row_height)
1293
- row_bottom = (row_index + 1) * self._row_height
1420
+ row_index = int((y - header_height + self._y_offset) // row_height)
1421
+ row_bottom = (row_index + 1) * row_height
1294
1422
  if row_bottom > self._y_offset + self._body_height():
1295
1423
  return None
1296
1424
  if 0 <= row_index < len(self._model.view_indices):
@@ -1313,19 +1441,58 @@ class CTkDataTable(ctk.CTkFrame):
1313
1441
  return None
1314
1442
 
1315
1443
  def _visible_columns(self) -> list[TableColumn]:
1316
- return self._visible_columns_cache
1444
+ return self._layout_columns_cache
1317
1445
 
1318
1446
  def _refresh_column_cache(self) -> None:
1319
1447
  visible_columns = [column for column in self._columns if column.visible]
1448
+ layout_widths = self._layout_column_widths(visible_columns)
1449
+ layout_columns = [
1450
+ replace(column, width=max(0, int(round(width))))
1451
+ for column, width in zip(visible_columns, layout_widths, strict=True)
1452
+ ]
1320
1453
  edges: list[float] = []
1321
1454
  cursor = 0.0
1322
- for column in visible_columns:
1323
- cursor += column.width
1455
+ for width in layout_widths:
1456
+ cursor += width
1324
1457
  edges.append(cursor)
1325
1458
  self._visible_columns_cache = visible_columns
1459
+ self._layout_columns_cache = layout_columns
1326
1460
  self._column_edges_cache = tuple(edges)
1327
1461
  self._column_edge_columns_cache = tuple(visible_columns)
1328
- self._total_table_width_cache = int(cursor)
1462
+ self._total_table_width_cache = cursor
1463
+
1464
+ def _layout_column_widths(self, visible_columns: Sequence[TableColumn]) -> list[float]:
1465
+ preferred_widths = [self._scale_dimension(column.width) for column in visible_columns]
1466
+ if not preferred_widths:
1467
+ return []
1468
+ if self._column_width_mode == "fixed" or self._canvas_width <= 1:
1469
+ return preferred_widths
1470
+
1471
+ viewport_width = float(self._canvas_width)
1472
+ preferred_total = sum(preferred_widths)
1473
+ if preferred_total <= 0:
1474
+ return [viewport_width / len(preferred_widths) for _column in preferred_widths]
1475
+
1476
+ if preferred_total < viewport_width:
1477
+ extra = viewport_width - preferred_total
1478
+ return [width + extra * (width / preferred_total) for width in preferred_widths]
1479
+
1480
+ min_width = self._scale_dimension(self._min_column_width)
1481
+ minimum_widths = [min(width, min_width) for width in preferred_widths]
1482
+ minimum_total = sum(minimum_widths)
1483
+ if minimum_total >= viewport_width:
1484
+ return minimum_widths
1485
+
1486
+ shrink = preferred_total - viewport_width
1487
+ capacities = [preferred - minimum for preferred, minimum in zip(preferred_widths, minimum_widths, strict=True)]
1488
+ total_capacity = sum(capacities)
1489
+ if total_capacity <= 0:
1490
+ return preferred_widths
1491
+
1492
+ return [
1493
+ preferred - shrink * (capacity / total_capacity)
1494
+ for preferred, capacity in zip(preferred_widths, capacities, strict=True)
1495
+ ]
1329
1496
 
1330
1497
  def _set_y_offset(self, value: float) -> None:
1331
1498
  old_offset = self._y_offset
@@ -1351,7 +1518,7 @@ class CTkDataTable(ctk.CTkFrame):
1351
1518
  0,
1352
1519
  0,
1353
1520
  max(self._canvas_width, self._total_table_width()),
1354
- self._header_height + self._total_body_height() + self._active_footer_height(),
1521
+ self._scaled_header_height() + self._total_body_height() + self._active_footer_height(),
1355
1522
  )
1356
1523
  )
1357
1524
 
@@ -1375,18 +1542,21 @@ class CTkDataTable(ctk.CTkFrame):
1375
1542
  self._horizontal_scrollbar.set(first, last)
1376
1543
 
1377
1544
  def _body_height(self) -> int:
1378
- return max(0, self._footer_top() - self._header_height)
1545
+ return max(0, int(self._footer_top() - self._scaled_header_height()))
1379
1546
 
1380
- def _active_footer_height(self) -> int:
1381
- return self._footer_height if self._footer_enabled else 0
1547
+ def _active_footer_height(self) -> float:
1548
+ return self._scaled_footer_height() if self._footer_enabled else 0.0
1382
1549
 
1383
- def _footer_top(self) -> int:
1384
- return max(self._header_height, self._canvas_height - self._active_footer_height() - self._bottom_cap_height())
1550
+ def _footer_top(self) -> float:
1551
+ return max(
1552
+ self._scaled_header_height(),
1553
+ self._canvas_height - self._active_footer_height() - self._bottom_cap_height(),
1554
+ )
1385
1555
 
1386
- def _total_body_height(self) -> int:
1387
- return len(self._model.view_indices) * self._row_height
1556
+ def _total_body_height(self) -> float:
1557
+ return len(self._model.view_indices) * self._scaled_row_height()
1388
1558
 
1389
- def _total_table_width(self) -> int:
1559
+ def _total_table_width(self) -> float:
1390
1560
  return self._total_table_width_cache
1391
1561
 
1392
1562
  def _max_y_offset(self) -> float:
@@ -1409,9 +1579,15 @@ class CTkDataTable(ctk.CTkFrame):
1409
1579
  return
1410
1580
  scrollbar_gap = self._scrollbar_gap()
1411
1581
  self._table_canvas.grid_configure(padx=0, pady=0)
1412
- self._vertical_scrollbar.grid_configure(padx=(scrollbar_gap, 0), pady=4)
1582
+ self._vertical_scrollbar.grid_configure(
1583
+ padx=(self._scale_dimension(scrollbar_gap), 0),
1584
+ pady=self._scale_dimension(4),
1585
+ )
1413
1586
  if self._horizontal_scrollbar is not None:
1414
- self._horizontal_scrollbar.grid_configure(padx=(0, scrollbar_gap), pady=(scrollbar_gap, 0))
1587
+ self._horizontal_scrollbar.grid_configure(
1588
+ padx=(0, self._scale_dimension(scrollbar_gap)),
1589
+ pady=(self._scale_dimension(scrollbar_gap), 0),
1590
+ )
1415
1591
 
1416
1592
  def _scrollbar_gap(self) -> int:
1417
1593
  radius = self._table_corner_radius()
@@ -1419,13 +1595,13 @@ class CTkDataTable(ctk.CTkFrame):
1419
1595
  return 0
1420
1596
  return max(4, min(8, math.ceil(radius / 2)))
1421
1597
 
1422
- def _bottom_cap_height(self) -> int:
1598
+ def _bottom_cap_height(self) -> float:
1423
1599
  if self._footer_enabled:
1424
1600
  return 0
1425
- radius = self._table_corner_radius()
1601
+ radius = self._scaled_table_corner_radius()
1426
1602
  if radius <= 0:
1427
1603
  return 0
1428
- return max(2, min(6, math.ceil(radius / 3)))
1604
+ return max(self._scale_dimension(2), min(self._scale_dimension(6), math.ceil(radius / 3)))
1429
1605
 
1430
1606
  def _apply_frame_style(self) -> None:
1431
1607
  self.configure(corner_radius=0, border_width=0, fg_color="transparent")
@@ -1442,6 +1618,7 @@ class CTkDataTable(ctk.CTkFrame):
1442
1618
  progress_radius=self._style_number("progress_radius"),
1443
1619
  pill_radius=self._style_number("pill_radius"),
1444
1620
  action_radius=self._style_number("action_radius"),
1621
+ scale=self._widget_scale(),
1445
1622
  )
1446
1623
 
1447
1624
  def _style_number(self, key: str) -> float | None:
@@ -1649,6 +1826,28 @@ class CTkDataTable(ctk.CTkFrame):
1649
1826
  except tk.TclError:
1650
1827
  return (127, 127, 127)
1651
1828
 
1829
+ def _set_scaling(self, *args: Any, **kwargs: Any) -> None:
1830
+ old_scale = self._widget_scale()
1831
+ logical_y_offset = self._y_offset / old_scale if hasattr(self, "_y_offset") else 0.0
1832
+ logical_x_offset = self._x_offset / old_scale if hasattr(self, "_x_offset") else 0.0
1833
+
1834
+ super()._set_scaling(*args, **kwargs)
1835
+
1836
+ if not hasattr(self, "_table_canvas"):
1837
+ return
1838
+ self._table_canvas.configure(
1839
+ width=self._scale_dimension(self._desired_width),
1840
+ height=self._scale_dimension(self._desired_height),
1841
+ )
1842
+ self._refresh_scaled_fonts()
1843
+ self._apply_renderer_style()
1844
+ self._apply_layout_insets()
1845
+ self._refresh_column_cache()
1846
+ self._y_offset = logical_y_offset * self._widget_scale()
1847
+ self._x_offset = logical_x_offset * self._widget_scale()
1848
+ self._clamp_offsets()
1849
+ self._redraw(full=True)
1850
+
1652
1851
  def _set_appearance_mode(self, mode_string: str) -> None:
1653
1852
  try:
1654
1853
  super()._set_appearance_mode(mode_string)
@@ -1657,3 +1856,7 @@ class CTkDataTable(ctk.CTkFrame):
1657
1856
  self._theme_colors_cache = None
1658
1857
  if hasattr(self, "_table_canvas"):
1659
1858
  self._redraw(full=True)
1859
+
1860
+ def destroy(self) -> None:
1861
+ self._remove_font_callbacks()
1862
+ super().destroy()
@@ -175,7 +175,8 @@ def main() -> None:
175
175
  app,
176
176
  columns=columns,
177
177
  data=rows,
178
- horizontal_scroll=False,
178
+ horizontal_scroll=True,
179
+ column_width_mode="fill",
179
180
  multi_select=False,
180
181
  searchable=False,
181
182
  search_delay_ms=120,
@@ -232,7 +232,7 @@ class Column(Mapping):
232
232
  return self
233
233
 
234
234
  def width(self, pixels: int) -> Column:
235
- """Set the column width in pixels."""
235
+ """Set the preferred column width in logical pixels."""
236
236
  self._data["width"] = pixels
237
237
  return self
238
238