monkeyqt 0.2.2__tar.gz → 0.2.4__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 (76) hide show
  1. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/PKG-INFO +1 -1
  2. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/__init__.py +7 -2
  3. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/basic/button.py +21 -4
  4. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/basic/select_card.py +57 -8
  5. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/__init__.py +2 -1
  6. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/box.py +100 -3
  7. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/window.py +113 -10
  8. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt.egg-info/PKG-INFO +1 -1
  9. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/LICENSE +0 -0
  10. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/README.md +0 -0
  11. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/common/enums.py +0 -0
  12. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/basic/__init__.py +0 -0
  13. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/basic/checkbox.py +0 -0
  14. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/__init__.py +0 -0
  15. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/avatar.py +0 -0
  16. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/console.py +0 -0
  17. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/data_table.py +0 -0
  18. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/image_compare.py +0 -0
  19. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/image_split.py +0 -0
  20. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/preview_dialogs.py +0 -0
  21. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/pro_table.py +0 -0
  22. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/data/table.py +0 -0
  23. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/__init__.py +0 -0
  24. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/alert.py +0 -0
  25. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/card.py +0 -0
  26. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/progress_bar.py +0 -0
  27. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/progress_ring.py +0 -0
  28. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/feedback/tooltip.py +0 -0
  29. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/auth.py +0 -0
  30. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/captcha.py +0 -0
  31. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/combobox.py +0 -0
  32. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/date_picker.py +0 -0
  33. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/form.py +0 -0
  34. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/input.py +0 -0
  35. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/multicombobox.py +0 -0
  36. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/slider.py +0 -0
  37. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/switch.py +0 -0
  38. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/form/upload.py +0 -0
  39. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/page.py +0 -0
  40. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/scroll_area.py +0 -0
  41. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/widget.py +0 -0
  42. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/layout/window_shell.py +0 -0
  43. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/__init__.py +0 -0
  44. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/avatar_menu.py +0 -0
  45. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/breadcrumb.py +0 -0
  46. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/dropdown.py +0 -0
  47. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/history.py +0 -0
  48. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/pagination.py +0 -0
  49. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/segmented.py +0 -0
  50. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/sidebar.py +0 -0
  51. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/tabs.py +0 -0
  52. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/components/navigation/topbar.py +0 -0
  53. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/core/icons.py +0 -0
  54. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/core/theme.py +0 -0
  55. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/core/theme_data.py +0 -0
  56. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/designer/plugins.py +0 -0
  57. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/icons.py +0 -0
  58. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/__init__.py +0 -0
  59. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/__main__.py +0 -0
  60. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/adapter.py +0 -0
  61. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/components/__init__.py +0 -0
  62. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/components/theme_selector.py +0 -0
  63. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/engine.py +0 -0
  64. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/gallery.py +0 -0
  65. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/manager.py +0 -0
  66. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/names.py +0 -0
  67. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/palette_theme.py +0 -0
  68. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/style_utils.py +0 -0
  69. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/tokens.py +0 -0
  70. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt/themes/unified_qss.py +0 -0
  71. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt.egg-info/SOURCES.txt +0 -0
  72. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt.egg-info/dependency_links.txt +0 -0
  73. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt.egg-info/requires.txt +0 -0
  74. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/monkeyqt.egg-info/top_level.txt +0 -0
  75. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/pyproject.toml +0 -0
  76. {monkeyqt-0.2.2 → monkeyqt-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyqt
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: An Enterprise-grade PySide6 UI Component Library
5
5
  Project-URL: Homepage, https://github.com/luohuabuxiema/MonkeyQt
6
6
  Requires-Python: >=3.8
@@ -33,6 +33,8 @@ from .components.form.multicombobox import MkMultiComboBox
33
33
 
34
34
  # Data
35
35
  from .components.data.avatar import MkAvatar
36
+ from .components.data.table import MkTable
37
+ from .components.data.data_table import MkDataTable
36
38
  from .components.data.pro_table import MkProTable
37
39
  from .components.data.image_compare import MkImageCompare
38
40
  from .components.data.image_split import MkImageSplit
@@ -56,7 +58,7 @@ from .components.feedback.tooltip import (
56
58
  # Layout
57
59
  from .components.layout.window import MkTitleBar, MkWindow
58
60
  from .components.layout.window_shell import MkWindowShell
59
- from .components.layout.box import MkQVBoxLayout, MkQHBoxLayout, MkVBox, MkHBox
61
+ from .components.layout.box import MkQVBoxLayout, MkQHBoxLayout, MkResponsiveBox, MkVBox, MkHBox
60
62
  from .components.layout.widget import MkQWidget, MkWidget
61
63
  from .components.layout.scroll_area import MkScrollArea, MkStackedWidget
62
64
  from .components.layout.page import MkPage
@@ -79,7 +81,7 @@ from .themes import (
79
81
  modern_scrollbar_qss,
80
82
  )
81
83
 
82
- __version__ = "0.2.2"
84
+ __version__ = "0.2.4"
83
85
 
84
86
  __all__ = [
85
87
  "MkButton",
@@ -113,6 +115,8 @@ __all__ = [
113
115
  "MkMessage",
114
116
  "MkUpload",
115
117
  "MkAvatar",
118
+ "MkTable",
119
+ "MkDataTable",
116
120
  "MkProTable",
117
121
  "MkImageCompare",
118
122
  "MkImageSplit",
@@ -133,6 +137,7 @@ __all__ = [
133
137
  "MkWindowShell",
134
138
  "MkQVBoxLayout",
135
139
  "MkQHBoxLayout",
140
+ "MkResponsiveBox",
136
141
  "MkVBox",
137
142
  "MkHBox",
138
143
  "MkQWidget",
@@ -214,10 +214,23 @@ class MkButton(QPushButton):
214
214
  qss += f"\nQPushButton {{ {size_qss} }}"
215
215
  _MkThemeEngine.apply_style_sheet(self, qss)
216
216
 
217
+ def sizeHint(self):
218
+ hint = super().sizeHint()
219
+ fm = self.fontMetrics()
220
+ text_w = fm.horizontalAdvance(self.text()) if self.text() else 0
221
+ icon_w = self.iconSize().width() if not self.icon().isNull() else 0
222
+ spacing = 8 if (icon_w and text_w) else 0
223
+ padding_h = 28 if text_w else 0
224
+ w = max(hint.width(), icon_w + text_w + spacing + padding_h)
225
+ h = max(hint.height(), 36)
226
+ from PySide6.QtCore import QSize
227
+ return QSize(w, h)
228
+
217
229
  def paintEvent(self, event):
218
230
  t = ThemeEngine
219
231
  painter = QPainter(self)
220
232
  painter.setRenderHint(QPainter.RenderHint.Antialiasing)
233
+ painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
221
234
  rect = self.rect()
222
235
  primary = t.get("--primary", "#409EFF")
223
236
  bg = t.get("--bg", "#FFFFFF")
@@ -411,7 +424,7 @@ class MkButton(QPushButton):
411
424
  icon_sz = self.iconSize()
412
425
  icon_w = icon_sz.width()
413
426
  icon_h = icon_sz.height()
414
- spacing = 6 if text else 0
427
+ spacing = 8 if text else 0
415
428
  fm = painter.fontMetrics()
416
429
  text_w = fm.horizontalAdvance(text) if text else 0
417
430
  total_w = icon_w + spacing + text_w
@@ -419,8 +432,7 @@ class MkButton(QPushButton):
419
432
  start_x = rect.left() + (rect.width() - total_w) // 2
420
433
  icon_y = rect.top() + (rect.height() - icon_h) // 2
421
434
 
422
- pixmap = icon.pixmap(icon_sz)
423
- painter.drawPixmap(start_x, icon_y, pixmap)
435
+ icon.paint(painter, QRect(start_x, icon_y, icon_w, icon_h))
424
436
 
425
437
  if text:
426
438
  text_rect = QRect(start_x + icon_w + spacing, rect.top(), text_w + 10, rect.height())
@@ -482,7 +494,7 @@ class MkButton(QPushButton):
482
494
  def setIcon(self, icon):
483
495
  if isinstance(icon, str):
484
496
  try:
485
- from monkeyqt.icons import Ph
497
+ from monkeyqt_icons import Ph
486
498
  pm = Ph.pixmap(icon, size=16, color=self._get_button_text_color())
487
499
  if pm and not pm.isNull():
488
500
  icon = QIcon(pm)
@@ -491,11 +503,15 @@ class MkButton(QPushButton):
491
503
  elif hasattr(icon, "_ph_spec"):
492
504
  self._ph_icon_spec = getattr(icon, "_ph_spec", None)
493
505
  try:
506
+ from monkeyqt_icons import Ph
494
507
  spec = dict(self._ph_icon_spec)
495
508
  orig_color = spec.get("color")
496
509
  if orig_color in (None, "auto", "default", "fg", "foreground", "currentcolor"):
497
510
  spec["color"] = self._get_button_text_color()
498
511
  icon = Ph.icon(**spec)
512
+ if "size" in spec and isinstance(spec["size"], int):
513
+ from PySide6.QtCore import QSize
514
+ self.setIconSize(QSize(spec["size"], spec["size"]))
499
515
  except Exception:
500
516
  pass
501
517
  super().setIcon(icon)
@@ -504,6 +520,7 @@ class MkButton(QPushButton):
504
520
  self._update_style()
505
521
  if getattr(self, "_ph_icon_spec", None):
506
522
  try:
523
+ from monkeyqt_icons import Ph
507
524
  spec = dict(self._ph_icon_spec)
508
525
  orig_color = spec.get("color")
509
526
  if orig_color in (None, "auto", "default", "fg", "foreground", "currentcolor"):
@@ -281,6 +281,7 @@ class MkSelectItem(QFrame):
281
281
 
282
282
  self.setCursor(Qt.CursorShape.PointingHandCursor)
283
283
  self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
284
+ self.setMinimumWidth(0)
284
285
  self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
285
286
 
286
287
  self._init_ui()
@@ -311,6 +312,10 @@ class MkSelectItem(QFrame):
311
312
 
312
313
  # 3. 文本列(主标题 + 副标题/指标)
313
314
  self.text_container = QWidget(self)
315
+ self.text_container.setMinimumWidth(0)
316
+ text_policy = self.text_container.sizePolicy()
317
+ text_policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored)
318
+ self.text_container.setSizePolicy(text_policy)
314
319
  _MkThemeEngine.apply_style_sheet(self.text_container, "background: transparent;")
315
320
  text_layout = QVBoxLayout(self.text_container)
316
321
  text_layout.setContentsMargins(0, 0, 0, 0)
@@ -321,6 +326,10 @@ class MkSelectItem(QFrame):
321
326
  title_font.setPointSize(10)
322
327
  title_font.setWeight(QFont.Weight.Normal)
323
328
  self.lbl_title.setFont(title_font)
329
+ self.lbl_title.setMinimumWidth(0)
330
+ title_policy = self.lbl_title.sizePolicy()
331
+ title_policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored)
332
+ self.lbl_title.setSizePolicy(title_policy)
324
333
  _MkThemeEngine.apply_style_sheet(self.lbl_title, "background: transparent;")
325
334
  text_layout.addWidget(self.lbl_title)
326
335
 
@@ -328,6 +337,10 @@ class MkSelectItem(QFrame):
328
337
  sub_font = QFont()
329
338
  sub_font.setPointSize(9)
330
339
  self.lbl_subtitle.setFont(sub_font)
340
+ self.lbl_subtitle.setMinimumWidth(0)
341
+ subtitle_policy = self.lbl_subtitle.sizePolicy()
342
+ subtitle_policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored)
343
+ self.lbl_subtitle.setSizePolicy(subtitle_policy)
331
344
  _MkThemeEngine.apply_style_sheet(self.lbl_subtitle, "background: transparent;")
332
345
  self.lbl_subtitle.setVisible(bool(self._subtitle_text))
333
346
  text_layout.addWidget(self.lbl_subtitle)
@@ -376,6 +389,26 @@ class MkSelectItem(QFrame):
376
389
  if self._actions_visible_on_hover:
377
390
  self.actions_container.setVisible(False)
378
391
 
392
+ def resizeEvent(self, event):
393
+ super().resizeEvent(event)
394
+ self._update_elided_text()
395
+
396
+ def _update_elided_text(self):
397
+ """Fit long labels into narrow cards without growing the scroll area."""
398
+ for label, source in (
399
+ (self.lbl_title, self._title_text),
400
+ (self.lbl_subtitle, self._subtitle_text),
401
+ ):
402
+ available = max(0, label.width() - 2)
403
+ display = label.fontMetrics().elidedText(
404
+ source,
405
+ Qt.TextElideMode.ElideRight,
406
+ available,
407
+ )
408
+ if label.text() != display:
409
+ label.setText(display)
410
+ label.setToolTip(source if display != source else "")
411
+
379
412
  def _update_indicator(self):
380
413
  """更新圆点或图标展示"""
381
414
  if self._dot_color:
@@ -558,12 +591,12 @@ class MkSelectItem(QFrame):
558
591
 
559
592
  def set_title(self, title: str):
560
593
  self._title_text = title
561
- self.lbl_title.setText(title)
594
+ self._update_elided_text()
562
595
 
563
596
  def set_subtitle(self, subtitle: str):
564
597
  self._subtitle_text = subtitle
565
- self.lbl_subtitle.setText(subtitle)
566
598
  self.lbl_subtitle.setVisible(bool(subtitle))
599
+ self._update_elided_text()
567
600
  self.setMinimumHeight(56)
568
601
 
569
602
  def set_dot_color(self, color: Optional[str]):
@@ -729,11 +762,11 @@ class MkSelectCard(QFrame):
729
762
 
730
763
  def __init__(
731
764
  self,
732
- title: str = "Models",
765
+ title: str = "训练任务与模型",
733
766
  icon: Optional[str] = "cube",
734
767
  show_select_all: bool = True,
735
- select_all_text: str = "Select all",
736
- badge_pattern: str = "{count} selected",
768
+ select_all_text: str = "全选 / 取消全选",
769
+ badge_pattern: str = "{count} 已选",
737
770
  max_list_height: Optional[int] = None,
738
771
  parent: Optional[QWidget] = None,
739
772
  ):
@@ -752,6 +785,8 @@ class MkSelectCard(QFrame):
752
785
  self._updating_select_all = False
753
786
 
754
787
  self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
788
+ self.setMinimumWidth(0)
789
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
755
790
  self._init_ui()
756
791
  ThemeEngine.instance().themeChanged.connect(self._apply_style)
757
792
  self._apply_style()
@@ -775,10 +810,13 @@ class MkSelectCard(QFrame):
775
810
  hdr_font.setPointSize(11)
776
811
  hdr_font.setWeight(QFont.Weight.Bold)
777
812
  self.lbl_hdr_title.setFont(hdr_font)
813
+ self.lbl_hdr_title.setMinimumWidth(0)
814
+ header_title_policy = self.lbl_hdr_title.sizePolicy()
815
+ header_title_policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored)
816
+ self.lbl_hdr_title.setSizePolicy(header_title_policy)
778
817
  _MkThemeEngine.apply_style_sheet(self.lbl_hdr_title, "background: transparent;")
779
- self.header_layout.addWidget(self.lbl_hdr_title)
780
-
781
- self.header_layout.addStretch()
818
+ # 标题本身承担弹性空间,避免独立 stretch 在窄屏下把标题和计数压成 0px。
819
+ self.header_layout.addWidget(self.lbl_hdr_title, stretch=1)
782
820
 
783
821
  # 右侧简约文本计数(参考图三: "1 selected",告别蓝色胶囊)
784
822
  self.lbl_badge = QLabel(self._badge_pattern.format(count=0), self)
@@ -787,6 +825,10 @@ class MkSelectCard(QFrame):
787
825
  badge_font.setWeight(QFont.Weight.Normal)
788
826
  self.lbl_badge.setFont(badge_font)
789
827
  self.lbl_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
828
+ self.lbl_badge.setMinimumWidth(0)
829
+ badge_policy = self.lbl_badge.sizePolicy()
830
+ badge_policy.setHorizontalPolicy(QSizePolicy.Policy.Minimum)
831
+ self.lbl_badge.setSizePolicy(badge_policy)
790
832
  self.header_layout.addWidget(self.lbl_badge)
791
833
 
792
834
  self._card_layout.addLayout(self.header_layout)
@@ -802,11 +844,18 @@ class MkSelectCard(QFrame):
802
844
  self.scroll_area = QScrollArea(self)
803
845
  self.scroll_area.setWidgetResizable(True)
804
846
  self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
847
+ self.scroll_area.setMinimumWidth(0)
848
+ self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
849
+ self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
805
850
  if self._max_list_height:
806
851
  self.scroll_area.setMaximumHeight(self._max_list_height)
807
852
 
808
853
  self.list_container = QWidget()
809
854
  self.list_container.setObjectName("MkSelectListContainer")
855
+ self.list_container.setMinimumWidth(0)
856
+ list_policy = self.list_container.sizePolicy()
857
+ list_policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored)
858
+ self.list_container.setSizePolicy(list_policy)
810
859
  self.list_layout = QVBoxLayout(self.list_container)
811
860
  self.list_layout.setContentsMargins(0, 0, 0, 0)
812
861
  self.list_layout.setSpacing(6)
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- from .box import MkQVBoxLayout, MkQHBoxLayout, MkVBox, MkHBox
2
+ from .box import MkQVBoxLayout, MkQHBoxLayout, MkResponsiveBox, MkVBox, MkHBox
3
3
  from .widget import MkQWidget, MkWidget
4
4
  from .window import MkTitleBar, MkWindow
5
5
  from .window_shell import MkWindowShell
@@ -9,6 +9,7 @@ from .page import MkPage
9
9
  __all__ = [
10
10
  "MkQVBoxLayout",
11
11
  "MkQHBoxLayout",
12
+ "MkResponsiveBox",
12
13
  "MkVBox",
13
14
  "MkHBox",
14
15
  "MkQWidget",
@@ -1,7 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
- from PySide6.QtCore import Qt, Property, QRectF
2
+ from typing import Optional
3
+
4
+ from PySide6.QtCore import Qt, Property, QRectF, Signal
3
5
  from PySide6.QtGui import QPainter, QColor, QPen, QBrush
4
- from PySide6.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QWidget
6
+ from PySide6.QtWidgets import QBoxLayout, QFrame, QVBoxLayout, QHBoxLayout, QWidget
5
7
 
6
8
  from monkeyqt.themes.engine import ThemeEngine
7
9
  from monkeyqt.themes.style_utils import qcolor
@@ -211,13 +213,33 @@ class MkQHBoxLayout(QFrame):
211
213
  支持自动填充/自适应大小、外边距(padding)、内间距(spacing/gap)、背景颜色、圆角以及可控制显示/隐藏的边框线。
212
214
  """
213
215
 
214
- def __init__(self, widgets=None, spacing=8, padding=12, border=False, border_width=1, radius=6, bg_color=None, parent=None):
216
+ responsiveChanged = Signal(bool)
217
+
218
+ def __init__(
219
+ self,
220
+ widgets=None,
221
+ spacing=8,
222
+ padding=12,
223
+ border=False,
224
+ border_width=1,
225
+ radius=6,
226
+ bg_color=None,
227
+ parent=None,
228
+ breakpoint: Optional[int] = None,
229
+ compact_spacing: Optional[int] = None,
230
+ ):
215
231
  super().__init__(parent)
216
232
  self._padding = padding
217
233
  self._border = border
218
234
  self._border_width = border_width
219
235
  self._radius = radius
220
236
  self._bg_color = bg_color
237
+ self._breakpoint = max(1, int(breakpoint)) if breakpoint else None
238
+ self._wide_spacing = int(spacing)
239
+ self._compact_spacing = (
240
+ self._wide_spacing if compact_spacing is None else int(compact_spacing)
241
+ )
242
+ self._is_compact = False
221
243
 
222
244
  self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
223
245
  self.setStyleSheet(f"""
@@ -241,6 +263,51 @@ class MkQHBoxLayout(QFrame):
241
263
 
242
264
  ThemeEngine.instance().themeChanged.connect(self.set_theme_style)
243
265
 
266
+ def showEvent(self, event):
267
+ super().showEvent(event)
268
+ self._sync_responsive_direction()
269
+
270
+ def resizeEvent(self, event):
271
+ super().resizeEvent(event)
272
+ self._sync_responsive_direction(event.size().width())
273
+
274
+ def _sync_responsive_direction(self, width: Optional[int] = None):
275
+ if self._breakpoint is None:
276
+ compact = False
277
+ else:
278
+ compact = (self.width() if width is None else width) < self._breakpoint
279
+
280
+ if compact == self._is_compact:
281
+ return
282
+
283
+ self._is_compact = compact
284
+ self._layout.setDirection(
285
+ QBoxLayout.Direction.TopToBottom
286
+ if compact
287
+ else QBoxLayout.Direction.LeftToRight
288
+ )
289
+ self._layout.setSpacing(
290
+ self._compact_spacing if compact else self._wide_spacing
291
+ )
292
+ self._layout.invalidate()
293
+ self.updateGeometry()
294
+ self.responsiveChanged.emit(compact)
295
+
296
+ def set_breakpoint(self, breakpoint: Optional[int]):
297
+ self._breakpoint = max(1, int(breakpoint)) if breakpoint else None
298
+ self._sync_responsive_direction()
299
+
300
+ def is_compact(self) -> bool:
301
+ return self._is_compact
302
+
303
+ @Property(int)
304
+ def breakpoint(self):
305
+ return self._breakpoint or 0
306
+
307
+ @breakpoint.setter
308
+ def breakpoint(self, value):
309
+ self.set_breakpoint(value)
310
+
244
311
  def set_theme_style(self, style_name: str = None):
245
312
  self.update()
246
313
 
@@ -391,6 +458,36 @@ class MkQHBoxLayout(QFrame):
391
458
  self.update()
392
459
 
393
460
 
461
+ class MkResponsiveBox(MkQHBoxLayout):
462
+ """Horizontal container that stacks its children below a width breakpoint."""
463
+
464
+ def __init__(
465
+ self,
466
+ widgets=None,
467
+ spacing=8,
468
+ padding=12,
469
+ border=False,
470
+ border_width=1,
471
+ radius=6,
472
+ bg_color=None,
473
+ parent=None,
474
+ breakpoint=900,
475
+ compact_spacing=None,
476
+ ):
477
+ super().__init__(
478
+ widgets=widgets,
479
+ spacing=spacing,
480
+ padding=padding,
481
+ border=border,
482
+ border_width=border_width,
483
+ radius=radius,
484
+ bg_color=bg_color,
485
+ parent=parent,
486
+ breakpoint=breakpoint,
487
+ compact_spacing=compact_spacing,
488
+ )
489
+
490
+
394
491
  # 便捷别名
395
492
  MkVBox = MkQVBoxLayout
396
493
  MkHBox = MkQHBoxLayout
@@ -28,8 +28,8 @@ class MkTitleBarCloseButton(QPushButton):
28
28
  - Windowed mode: Smooth rounded top-right corner matching window border-radius (default 8px).
29
29
  - Maximized mode: Straight 90-degree right angle (0px) honoring Fitts's Law.
30
30
  2. Pixel-perfect seamless fitting:
31
- - Uses QPainter.CompositionMode_Source to eliminate any underlying titlebar/container
32
- background fringe or color leak.
31
+ - Clears alpha only for translucent fallback windows; DWM-clipped opaque
32
+ windows preserve the titlebar background to avoid black corner pixels.
33
33
  - High-DPI anti-aliased geometry drawing.
34
34
  """
35
35
  def __init__(self, titlebar, parent=None):
@@ -61,12 +61,16 @@ class MkTitleBarCloseButton(QPushButton):
61
61
  win = self.window()
62
62
  if win and hasattr(win, "isMaximized") and win.isMaximized():
63
63
  return 0.0
64
+ if win and getattr(win, "_use_native_corner_surface", False):
65
+ return 0.0
64
66
  p_win = getattr(self._titlebar, "parent_window", None)
65
67
  if p_win:
66
68
  if hasattr(p_win, "isMaximized") and p_win.isMaximized():
67
69
  return 0.0
68
70
  if getattr(p_win, "_current_is_max_state", False):
69
71
  return 0.0
72
+ if getattr(p_win, "_use_native_corner_surface", False):
73
+ return 0.0
70
74
  if hasattr(p_win, "_border_radius"):
71
75
  return float(p_win._border_radius)
72
76
 
@@ -116,14 +120,25 @@ class MkTitleBarCloseButton(QPushButton):
116
120
 
117
121
  if self._is_hovered or self._is_pressed:
118
122
  bg_color = QColor("#c42b1c" if self._is_pressed else "#e81123")
119
- # 1. Clear any parent titlebar background using Source mode to prevent edge leak
120
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
121
- painter.fillRect(self.rect(), QColor(0, 0, 0, 0))
123
+ host_window = getattr(self._titlebar, "parent_window", None)
124
+ native_corner_surface = bool(
125
+ getattr(host_window, "_use_native_corner_surface", False)
126
+ )
127
+ # Clearing to transparent is required by the legacy alpha-window
128
+ # path, but creates opaque black pixels in a DWM-clipped surface.
129
+ # The native path keeps the already-painted titlebar background.
130
+ if not native_corner_surface:
131
+ painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
132
+ painter.fillRect(self.rect(), QColor(0, 0, 0, 0))
122
133
 
123
134
  painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
124
- if r <= 0.0:
135
+ if r <= 0.0 or native_corner_surface:
136
+ # On Windows 11 DWM-clipped surfaces, the close button fills the full
137
+ # corner rectangle so DWM clips the window boundary natively without
138
+ # exposing any underlying titlebar background sliver.
125
139
  painter.fillRect(self.rect(), bg_color)
126
140
  else:
141
+ # Translucent fallback windows on legacy systems require manual corner curve
127
142
  painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
128
143
  path = QPainterPath()
129
144
  path.moveTo(0, 0)
@@ -754,6 +769,11 @@ class MkWindow(QMainWindow):
754
769
  self._auto_content_container = None
755
770
  self._close_behavior = "close" # "close" or "hide"
756
771
  self._border_radius = 8
772
+ # Windows 11 can let DWM clip an opaque frameless window. This avoids
773
+ # the layered-window trails seen while shrinking a translucent window,
774
+ # while older platforms keep the original alpha-based corner path.
775
+ self._use_native_corner_surface = self._supports_native_corner_surface()
776
+ self._native_frame_synced = False
757
777
  self._normal_geometry = None
758
778
  self._is_maximizing = False
759
779
 
@@ -806,10 +826,23 @@ class MkWindow(QMainWindow):
806
826
  self.setWindowFlags(Qt.WindowType.Window)
807
827
  self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False)
808
828
 
829
+ @staticmethod
830
+ def _supports_native_corner_surface() -> bool:
831
+ if sys.platform != "win32":
832
+ return False
833
+ try:
834
+ # DWMWA_WINDOW_CORNER_PREFERENCE is supported from build 22000.
835
+ return sys.getwindowsversion().build >= 22000
836
+ except (AttributeError, OSError):
837
+ return False
838
+
809
839
  def init_custom_frame(self):
810
840
  # Custom frameless behavior
811
841
  self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Window)
812
- self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
842
+ self.setAttribute(
843
+ Qt.WidgetAttribute.WA_TranslucentBackground,
844
+ not self._use_native_corner_surface,
845
+ )
813
846
 
814
847
  # 1. Root outer layout to support padding for drop shadow
815
848
  self._root_widget = QWidget(self)
@@ -887,6 +920,11 @@ class MkWindow(QMainWindow):
887
920
  super().setCentralWidget(self._root_widget)
888
921
 
889
922
  self.update_style()
923
+ if self._use_native_corner_surface and not self._enable_native_corners(False):
924
+ # Unsupported DWM implementations (older systems, compatibility
925
+ # layers) retain the original translucent corner implementation.
926
+ self._use_native_corner_surface = False
927
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
890
928
 
891
929
  def set_preset(self, preset: str):
892
930
  self._preset = preset
@@ -1541,8 +1579,7 @@ class MkWindow(QMainWindow):
1541
1579
  return None
1542
1580
 
1543
1581
  def _enable_native_corners(self, is_max: bool | None = None):
1544
- import sys
1545
- if sys.platform == "win32":
1582
+ if getattr(self, "_use_native_corner_surface", False):
1546
1583
  try:
1547
1584
  import ctypes
1548
1585
  hwnd = getattr(self, "_mk_cached_hwnd", None)
@@ -1556,7 +1593,14 @@ class MkWindow(QMainWindow):
1556
1593
  # 2 = DWMWCP_ROUND (Standard Windows 11 rounded corners)
1557
1594
  # 3 = DWMWCP_ROUNDSMALL (Restrained Windows 11 rounded corners)
1558
1595
  pref = ctypes.c_int(1 if is_max else 2)
1559
- ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 33, ctypes.byref(pref), ctypes.sizeof(pref))
1596
+ result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
1597
+ hwnd,
1598
+ 33,
1599
+ ctypes.byref(pref),
1600
+ ctypes.sizeof(pref),
1601
+ )
1602
+ if result != 0:
1603
+ return False
1560
1604
 
1561
1605
  border_color = ctypes.c_uint(0xFFFFFFFE) # DWMWA_COLOR_NONE
1562
1606
  ctypes.windll.dwmapi.DwmSetWindowAttribute(
@@ -1565,8 +1609,58 @@ class MkWindow(QMainWindow):
1565
1609
  ctypes.byref(border_color),
1566
1610
  ctypes.sizeof(border_color),
1567
1611
  )
1612
+ return True
1613
+ except Exception:
1614
+ return False
1615
+ return False
1616
+
1617
+ def _notify_native_frame_changed(self) -> bool:
1618
+ """Force one Win32 non-client recalculation after the first show."""
1619
+ if sys.platform != "win32" or not self._use_native_corner_surface:
1620
+ return False
1621
+ try:
1622
+ hwnd = getattr(self, "_mk_cached_hwnd", None)
1623
+ if hwnd is None:
1624
+ hwnd = int(self.winId())
1625
+ self._mk_cached_hwnd = hwnd
1626
+
1627
+ set_window_pos = ctypes.windll.user32.SetWindowPos
1628
+ set_window_pos.argtypes = [
1629
+ wintypes.HWND,
1630
+ wintypes.HWND,
1631
+ ctypes.c_int,
1632
+ ctypes.c_int,
1633
+ ctypes.c_int,
1634
+ ctypes.c_int,
1635
+ wintypes.UINT,
1636
+ ]
1637
+ set_window_pos.restype = wintypes.BOOL
1638
+
1639
+ # SWP_FRAMECHANGED sends WM_NCCALCSIZE even though position and
1640
+ # dimensions stay unchanged. This is the frame refresh otherwise
1641
+ # observed only after the first maximize/restore cycle.
1642
+ flags = 0x0001 | 0x0002 | 0x0004 | 0x0010 | 0x0020
1643
+ res = bool(set_window_pos(hwnd, 0, 0, 0, 0, 0, flags))
1644
+ try:
1645
+ ctypes.windll.user32.RedrawWindow(hwnd, None, None, 0x0401) # RDW_FRAME | RDW_INVALIDATE
1568
1646
  except Exception:
1569
1647
  pass
1648
+ return res
1649
+ except Exception:
1650
+ return False
1651
+
1652
+ def _refresh_native_chrome(self):
1653
+ """Re-apply DWM clipping after the HWND has entered the shown state."""
1654
+ if not getattr(self, "_use_native_corner_surface", False):
1655
+ return
1656
+ try:
1657
+ is_max = self.isMaximized()
1658
+ if self.titlebar:
1659
+ self.titlebar.set_maximized_state(is_max)
1660
+ self._notify_native_frame_changed()
1661
+ self._enable_native_corners(is_max)
1662
+ except RuntimeError:
1663
+ pass
1570
1664
 
1571
1665
  def _apply_window_state_immediate(self, is_max: bool, force: bool = False):
1572
1666
  try:
@@ -1682,6 +1776,15 @@ class MkWindow(QMainWindow):
1682
1776
  self._normal_geometry = self._get_safe_normal_geometry(self.geometry())
1683
1777
  is_max = True if getattr(self, "_is_maximizing", False) else self.isMaximized()
1684
1778
  self._apply_window_state_immediate(is_max, force=True)
1779
+ if self._use_native_corner_surface:
1780
+ # A custom frameless HWND needs one explicit non-client refresh on
1781
+ # first show. Re-apply the DWM preference afterwards because frame
1782
+ # recalculation may recreate its visible corner state.
1783
+ if not self._native_frame_synced:
1784
+ self._native_frame_synced = self._notify_native_frame_changed()
1785
+ self._enable_native_corners(is_max)
1786
+ QTimer.singleShot(0, self._refresh_native_chrome)
1787
+ QTimer.singleShot(80, self._refresh_native_chrome)
1685
1788
 
1686
1789
  def resizeEvent(self, event):
1687
1790
  super().resizeEvent(event)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monkeyqt
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: An Enterprise-grade PySide6 UI Component Library
5
5
  Project-URL: Homepage, https://github.com/luohuabuxiema/MonkeyQt
6
6
  Requires-Python: >=3.8
File without changes
File without changes
File without changes
File without changes
File without changes