ranking.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import abc
  2. import tkinter as tk
  3. from PIL import Image, ImageTk
  4. import conf
  5. from tool.type_ import *
  6. from tool.tk import make_font
  7. from sql.db import DB
  8. class RankingStationException(Exception):
  9. ...
  10. class RankingError(RankingStationException):
  11. ...
  12. class RankingPageError(RankingStationException):
  13. ...
  14. class RankingStationBase(metaclass=abc.ABCMeta):
  15. """
  16. RankingStation基类
  17. 封装排行榜相关操作
  18. """
  19. def __init__(self, db: DB):
  20. self._db = db
  21. self.rank = [[]]
  22. self.rank_index = 0
  23. self.rank_count = 1
  24. self.offset = 0
  25. self.limit_n = 2 # 一次性获取的数据 (页数, 行数=limit_n * rank_count)
  26. self.auto: bool = False
  27. self.auto_to_next: bool = True # auto的移动方向
  28. self.auto_time: int = 5000 # 5s
  29. def get_rank(self, offset: int = 0) -> Tuple[bool, list]:
  30. """
  31. 获取数据
  32. :param offset: 位移 (相对于当前位置移动的页数)
  33. :return: 成功, 排行榜数据
  34. """
  35. limit = self.rank_count * self.limit_n
  36. offset = self.offset + limit * offset # offset为0表示不移动, 1表示向前, -1表示向后
  37. cur = self._db.search((f"SELECT UserID, Name, Score, Reputation "
  38. f"FROM user "
  39. f"WHERE IsManager = 0 "
  40. f"ORDER BY reputation DESC, score DESC "
  41. f"LIMIT {limit} OFFSET {offset};"))
  42. if cur is None or cur.rowcount == 0:
  43. return False, []
  44. self.offset = offset
  45. rank_list = list(cur.fetchall())
  46. rank = [[]]
  47. for i, r in enumerate(rank_list):
  48. if len(rank[-1]) == self.rank_count:
  49. rank.append([])
  50. color = None
  51. if self.offset + i == 0:
  52. color = "#eaff56"
  53. elif self.offset + i == 1:
  54. color = "#ffa631"
  55. elif self.offset + i == 2:
  56. color = "#ff7500"
  57. rank[-1].append((self.offset + i + 1, r[1], r[0], r[2], r[3], color))
  58. return True, rank
  59. def rank_page_to_next(self):
  60. """
  61. 显示排行榜下一页数据
  62. :return:
  63. """
  64. if self.rank_index == len(self.rank) - 1:
  65. self.set_next_btn(True) # 当 rank_index为最后一项时, 该函数不应该被调用(除非数据库被外部改动)
  66. return
  67. self.rank_index += 1
  68. if self.rank_index == len(self.rank) - 1: # 最后一项
  69. if len(self.rank[self.rank_index]) == self.rank_count:
  70. res, rank = self.get_rank(1) # 向前移动一个offset
  71. if res:
  72. self.rank = self.rank[self.rank_index], *rank # 调整rank的内容
  73. self.rank_index = 0
  74. else:
  75. self.set_next_btn(True)
  76. else: # 如果最后一个表格没有填满, 直接判定无next
  77. self.set_next_btn(True)
  78. self.show_rank(self.rank[self.rank_index])
  79. self.set_prev_btn(False)
  80. if self.auto:
  81. self.set_run_after_now(self.auto_time, self.update_rank_auto)
  82. def rank_page_to_prev(self):
  83. """
  84. 显示排行榜上一页数据
  85. :return:
  86. """
  87. if self.rank_index == 0: # 当 rank_index为最后一项时, 该函数不应该被调用(除非数据库被外部改动)
  88. self.set_prev_btn(True)
  89. return
  90. self.rank_index -= 1
  91. if self.rank_index == 0: # 回到第一项
  92. res, rank = self.get_rank(-1) # 向后移动一个offset
  93. if res:
  94. self.rank = *rank, self.rank[self.rank_index] # 调整rank的内容
  95. self.rank_index = 0
  96. else:
  97. self.set_prev_btn(True)
  98. self.show_rank(self.rank[self.rank_index])
  99. self.set_next_btn(False)
  100. if self.auto:
  101. self.set_run_after_now(self.auto_time, self.update_rank_auto)
  102. def show_rank_first(self):
  103. """
  104. 第一次显示排行榜数据
  105. :return:
  106. """
  107. self.rank_index = 0
  108. self.offset = 0
  109. self.rank_count = self.rank_count
  110. res, self.rank = self.get_rank(0)
  111. self.show_rank(self.rank[0])
  112. self.set_prev_btn(True)
  113. if len(self.rank) == 1:
  114. self.set_next_btn(True)
  115. else:
  116. self.set_next_btn(False)
  117. def rank_auto(self, auto):
  118. """
  119. 启用或关闭自动显示
  120. :param auto: True-启用自动显示, False-关闭
  121. :return:
  122. """
  123. if auto:
  124. self.auto = True
  125. self.disable_btn()
  126. self.set_run_after_now(self.auto_time, self.update_rank_auto) # 注册自动函数
  127. else:
  128. self.auto = False
  129. self.able_btn()
  130. def update_rank_auto(self):
  131. """
  132. 自动显示
  133. :return:
  134. """
  135. if not self.auto:
  136. return
  137. if (self.auto_to_next and self.is_able_next() or
  138. not self.auto_to_next and not self.is_able_prev() and self.is_able_next()):
  139. self.rank_page_to_next()
  140. self.auto_to_next = True
  141. elif (not self.auto_to_next and self.is_able_prev() or
  142. self.auto_to_next and not self.is_able_next() and self.is_able_prev()):
  143. self.rank_page_to_prev()
  144. self.auto_to_next = False
  145. else:
  146. return # 无法动弹
  147. @abc.abstractmethod
  148. def show_rank(self, rank_info: List[Tuple[int, uname_t, uid_t, score_t, score_t, Optional[str]]]):
  149. ...
  150. @abc.abstractmethod
  151. def set_next_btn(self, disable: False):
  152. ...
  153. @abc.abstractmethod
  154. def set_prev_btn(self, disable: False):
  155. ...
  156. @abc.abstractmethod
  157. def disable_btn(self):
  158. ...
  159. @abc.abstractmethod
  160. def able_btn(self):
  161. ...
  162. @abc.abstractmethod
  163. def is_able_prev(self):
  164. ...
  165. @abc.abstractmethod
  166. def is_able_next(self):
  167. ...
  168. @abc.abstractmethod
  169. def set_run_after_now(self, ms, func, *args):
  170. ...
  171. @abc.abstractmethod
  172. def mainloop(self):
  173. ...
  174. class RankingStation(RankingStationBase):
  175. """
  176. RankingStation 排行榜系统
  177. 使用tkinter作为GUI
  178. 派生自 GarbageStation
  179. """
  180. def __init__(self, db: DB):
  181. self.init_after_run_list: List[Tuple[int, Callable, Tuple]] = []
  182. super(RankingStation, self).__init__(db)
  183. self.window = tk.Tk()
  184. self._sys_height = self.window.winfo_screenheight()
  185. self._sys_width = self.window.winfo_screenwidth()
  186. self.height = int(self._sys_height * (2 / 3))
  187. self.width = int(self._sys_width * (1 / 3))
  188. self._full_screen = False
  189. self.__conf_windows()
  190. self.next_btn: bool = True # 表示按键是否启用
  191. self.prev_btn: bool = True
  192. self.__conf_font_size()
  193. self.__create_tk()
  194. self.__conf_tk()
  195. self.show_rank_first()
  196. def __create_tk(self):
  197. self.rank_frame = tk.Frame(self.window)
  198. self.rank_title = tk.Label(self.rank_frame)
  199. self.rank_title_var = tk.StringVar()
  200. self.rank_count = 7 # 一页显示的行数
  201. self.rank_label = [tk.Label(self.rank_frame) for _ in range(self.rank_count)]
  202. self.rank_y_height: List[Tuple[float, float]] = [] # rank 标签的y坐标信息
  203. self.rank_var = [tk.StringVar() for _ in range(self.rank_count)]
  204. self.rank_btn = [tk.Button(self.rank_frame) for _ in range(3)] # prev, auto, next
  205. def __conf_font_size(self, n: Union[int, float] = 1):
  206. self._rank_font_title_size = int(24 * n)
  207. self._rank_font_size = int(16 * n)
  208. self._rank_font_btn_size = int(16 * n)
  209. def __conf_tk(self):
  210. self.__conf_windows_bg()
  211. self.__conf_rank()
  212. def __conf_windows(self):
  213. self.window.title('HGSSystem: Ranking')
  214. self.window.geometry(f'{self.width}x{self.height}')
  215. self.window['bg'] = conf.tk_win_bg
  216. self.window.resizable(False, False)
  217. self.bg_img = None
  218. self.bg_lb = tk.Label(self.window)
  219. self.bg_lb['bg'] = conf.tk_win_bg
  220. self.bg_lb.place(relx=0, rely=0, relwidth=1, relheight=1)
  221. self.window.bind("<F11>", lambda _: self.__switch_full_screen())
  222. def __conf_windows_bg(self):
  223. img = Image.open(conf.pic_d['rank_bg']).resize((self.width, self.height))
  224. self.bg_img = ImageTk.PhotoImage(img)
  225. self.bg_lb['image'] = self.bg_img
  226. self.bg_lb.place(relx=0, rely=0, relwidth=1, relheight=1)
  227. def __switch_full_screen(self):
  228. self._full_screen = not self._full_screen
  229. self.window.attributes("-fullscreen", self._full_screen)
  230. width = self._sys_width * (1 / 3)
  231. height = self._sys_height * (2 / 3)
  232. self.width = self.window.winfo_width()
  233. self.height = self.window.winfo_height()
  234. n = min((self.height / height), (self.width / width)) # 因为横和纵不是平均放大, 因此取倍数小的
  235. self.__conf_font_size(n)
  236. self.__conf_tk()
  237. self.show_rank_first()
  238. def __conf_rank(self):
  239. title_font = make_font(size=self._rank_font_title_size, weight="bold")
  240. info_font = make_font(size=self._rank_font_size)
  241. btn_font = make_font(size=self._rank_font_btn_size)
  242. height = self.height * 0.95
  243. width = height * (3 / 4)
  244. # 宽度过大
  245. if width >= self.width:
  246. width = self.width * 0.95
  247. height = width * (4 / 3) / self.width
  248. relwidth = width / self.width
  249. relheight = height / self.height
  250. relx = (1 - relwidth) / 2
  251. rely = (1 - relheight) / 2
  252. self.rank_frame['relief'] = "ridge"
  253. self.rank_frame['bd'] = 5
  254. self.rank_frame['bg'] = conf.tk_second_win_bg
  255. self.rank_frame.place(relx=relx, rely=rely, relwidth=relwidth, relheight=relheight)
  256. self.rank_title['font'] = title_font
  257. self.rank_title['bg'] = conf.tk_second_win_bg
  258. self.rank_title['textvariable'] = self.rank_title_var
  259. self.rank_title.place(relx=0.02, rely=0.00, relwidth=0.96, relheight=0.1)
  260. self.rank_title_var.set("排行榜加载中...")
  261. """
  262. 标签所拥有的总高度为0.82
  263. 标签数为c
  264. 间隔数为c-1
  265. 标签高度为x
  266. 间隔高度为y
  267. 规定 5y = x
  268. 所以: cx + (c - 1)y = 0.82
  269. 得到: 5cx + (c - 1)y = (5c + c - 1)y = 0.82
  270. 因此: x = 0.82 / (5c + c - 1)
  271. """
  272. height_s = (0.82 / (self.rank_count * 6 - 1)) # 标签的高度
  273. height_l = height_s * 5 # 间隔的高度
  274. height = 0.10 # 从 0.10 到 0.82
  275. for i in range(self.rank_count):
  276. self.rank_label[i]['font'] = info_font
  277. self.rank_label[i]['bg'] = "#F5FFFA"
  278. self.rank_label[i]['textvariable'] = self.rank_var[i]
  279. self.rank_label[i]['relief'] = "ridge"
  280. self.rank_label[i]['bd'] = 2
  281. self.rank_var[i].set("")
  282. self.rank_y_height.append((height, height_l))
  283. height += height_l + height_s
  284. for btn, text, x in zip(self.rank_btn,
  285. ("前一页", "手动" if self.auto else "自动", "下一页"), (0.050, 0.375, 0.700)):
  286. btn['font'] = btn_font
  287. btn['text'] = text
  288. btn['bg'] = conf.tk_btn_bg
  289. btn.place(relx=x, rely=0.93, relwidth=0.25, relheight=0.06)
  290. self.rank_btn[0]['command'] = lambda: self.rank_page_to_prev()
  291. self.rank_btn[1]['command'] = lambda: self.rank_auto(True)
  292. self.rank_btn[2]['command'] = lambda: self.rank_page_to_next()
  293. def set_run_after_now(self, ms, func, *args):
  294. self.window.after(ms, func, *args)
  295. def __set_rank_info(self, rank_info: List[Tuple[int, uname_t, uid_t, score_t, score_t, Optional[str]]]):
  296. if len(rank_info) > self.rank_count:
  297. rank_info = rank_info[:self.rank_count]
  298. for lb in self.rank_label: # 隐藏全部标签
  299. lb.place_forget()
  300. for i, info in enumerate(rank_info):
  301. no, name, uid, score, eval_, color = info
  302. self.rank_var[i].set(f"NO.{no} {name}\n\n" # 中间空一行 否则中文字体显得很窄
  303. f"ID: {uid[0:conf.ranking_tk_show_uid_len]} "
  304. f"信用: {eval_} 积分: {score}")
  305. if color is None:
  306. self.rank_label[i]['bg'] = "#F5FFFA"
  307. else:
  308. self.rank_label[i]['bg'] = color
  309. rely = self.rank_y_height[i][0]
  310. relheight = self.rank_y_height[i][1]
  311. self.rank_label[i].place(relx=0.04, rely=rely, relwidth=0.92, relheight=relheight)
  312. def show_rank(self, rank_info: List[Tuple[int, uname_t, uid_t, score_t, score_t, Optional[str]]]):
  313. self.__set_rank_info(rank_info)
  314. self.rank_title_var.set("排行榜")
  315. def rank_auto(self, auto):
  316. super(RankingStation, self).rank_auto(auto)
  317. if auto:
  318. self.rank_btn[1]['command'] = lambda: self.rank_auto(False)
  319. self.rank_btn[1]['text'] = '手动'
  320. else:
  321. self.rank_btn[1]['command'] = lambda: self.rank_auto(True)
  322. self.rank_btn[1]['text'] = '自动'
  323. def set_next_btn(self, disable: False):
  324. if disable or self.auto: # auto 模式令btn失效
  325. self.rank_btn[2]['state'] = 'disable'
  326. else:
  327. self.rank_btn[2]['state'] = 'normal'
  328. self.next_btn = not disable
  329. def set_prev_btn(self, disable: False):
  330. if disable or self.auto:
  331. self.rank_btn[0]['state'] = 'disable'
  332. else:
  333. self.rank_btn[0]['state'] = 'normal'
  334. self.prev_btn = not disable
  335. def disable_btn(self):
  336. self.rank_btn[0]['state'] = 'disable'
  337. self.rank_btn[2]['state'] = 'disable'
  338. def able_btn(self):
  339. if self.prev_btn:
  340. self.rank_btn[0]['state'] = 'normal'
  341. if self.next_btn:
  342. self.rank_btn[2]['state'] = 'normal'
  343. def is_able_prev(self):
  344. return self.prev_btn
  345. def is_able_next(self):
  346. return self.next_btn
  347. def mainloop(self):
  348. self.window.mainloop()
  349. if __name__ == '__main__':
  350. mysql_db = DB()
  351. station = RankingStation(mysql_db)
  352. station.mainloop()