ColumnarFlowLayout.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import sys
  2. from PyQt5.QtCore import QPoint, QRect, QSize, Qt
  3. from PyQt5.QtWidgets import QLayout, QSizePolicy
  4. import math
  5. class ColumnarFlowLayout(QLayout):
  6. def __init__(self, parent=None, margin=0, spacing=-1):
  7. super().__init__(parent)
  8. if parent is not None:
  9. self.setContentsMargins(margin, margin, margin, margin)
  10. self.setSpacing(spacing)
  11. self.itemList = []
  12. def __del__(self):
  13. item = self.takeAt(0)
  14. while item:
  15. item = self.takeAt(0)
  16. def addItem(self, item):
  17. self.itemList.append(item)
  18. def count(self):
  19. return len(self.itemList)
  20. def itemAt(self, index):
  21. if 0 <= index < len(self.itemList):
  22. return self.itemList[index]
  23. return None
  24. def takeAt(self, index):
  25. if 0 <= index < len(self.itemList):
  26. return self.itemList.pop(index)
  27. return None
  28. def expandingDirections(self):
  29. return Qt.Orientations(Qt.Orientation(0))
  30. def hasHeightForWidth(self):
  31. return True
  32. def heightForWidth(self, width):
  33. height = self.doLayout(QRect(0, 0, width, 0), True)
  34. return height
  35. def setGeometry(self, rect):
  36. super().setGeometry(rect)
  37. self.doLayout(rect, False)
  38. def sizeHint(self):
  39. return self.minimumSize()
  40. def minimumSize(self):
  41. size = QSize()
  42. for item in self.itemList:
  43. size = size.expandedTo(item.minimumSize())
  44. margin, _, _, _ = self.getContentsMargins()
  45. size += QSize(2 * margin, 2 * margin)
  46. return size
  47. def doLayout(self, rect: QRect, testOnly: bool) -> int:
  48. spacing = self.spacing()
  49. x = rect.x()
  50. y = rect.y()
  51. # Determine width of widest item
  52. widest = 0
  53. for item in self.itemList:
  54. widest = max(widest, item.sizeHint().width())
  55. # Determine how many equal-width columns we can get, and how wide each one should be
  56. column_count = math.floor(rect.width() / (widest + spacing))
  57. column_count = min(column_count, len(self.itemList))
  58. column_count = max(1, column_count)
  59. column_width = math.floor((rect.width() - (column_count-1)*spacing - 1) / column_count)
  60. # Get the heights for all of our items
  61. item_heights = {}
  62. for item in self.itemList:
  63. height = item.heightForWidth(column_width) if item.hasHeightForWidth() else item.sizeHint().height()
  64. item_heights[item] = height
  65. # Prepare our column representation
  66. column_contents = []
  67. column_heights = []
  68. for column_index in range(column_count):
  69. column_contents.append([])
  70. column_heights.append(0)
  71. def add_to_column(column: int, item):
  72. column_contents[column].append(item)
  73. column_heights[column] += (item_heights[item] + spacing)
  74. def shove_one(from_column: int) -> bool:
  75. if len(column_contents[from_column]) >= 1:
  76. item = column_contents[from_column].pop(0)
  77. column_heights[from_column] -= (item_heights[item] + spacing)
  78. add_to_column(from_column-1, item)
  79. return True
  80. return False
  81. def shove_cascade_consider(from_column: int) -> bool:
  82. changed = False
  83. if len(column_contents[from_column]) > 1:
  84. item = column_contents[from_column][0]
  85. item_height = item_heights[item]
  86. if column_heights[from_column-1] + item_height < max(column_heights):
  87. changed = shove_one(from_column) or changed
  88. if from_column+1 < column_count:
  89. changed = shove_cascade_consider(from_column+1) or changed
  90. return changed
  91. def shove_cascade() -> bool:
  92. if column_count < 2:
  93. return False
  94. changed = True
  95. while changed:
  96. changed = shove_cascade_consider(1)
  97. return changed
  98. def pick_best_shoving_position() -> int:
  99. best_pos = 1
  100. best_height = sys.maxsize
  101. for column_index in range(1, column_count):
  102. if len(column_contents[column_index]) == 0:
  103. continue
  104. item = column_contents[column_index][0]
  105. height_after_shove = column_heights[column_index-1] + item_heights[item]
  106. if height_after_shove < best_height:
  107. best_height = height_after_shove
  108. best_pos = column_index
  109. return best_pos
  110. # Calculate the best layout
  111. column_index = 0
  112. for item in self.itemList:
  113. item_height = item_heights[item]
  114. if column_heights[column_index] != 0 and (column_heights[column_index] + item_height) > max(column_heights):
  115. column_index += 1
  116. if column_index >= column_count:
  117. # Run out of room, need to shove more stuff in each column
  118. if column_count >= 2:
  119. changed = shove_cascade()
  120. if not changed:
  121. shoving_pos = pick_best_shoving_position()
  122. shove_one(shoving_pos)
  123. shove_cascade()
  124. column_index = column_count-1
  125. add_to_column(column_index, item)
  126. shove_cascade()
  127. # Set geometry according to the layout we have calculated
  128. if not testOnly:
  129. for column_index, items in enumerate(column_contents):
  130. x = column_index * (column_width + spacing)
  131. y = 0
  132. for item in items:
  133. height = item_heights[item]
  134. item.setGeometry(QRect(x, y, column_width, height))
  135. y += (height + spacing)
  136. # Return the overall height
  137. return max(column_heights)