ctk_canvas.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import tkinter
  2. import sys
  3. from typing import Union, Tuple
  4. class CTkCanvas(tkinter.Canvas):
  5. """
  6. Canvas with additional functionality to draw antialiased circles on Windows/Linux.
  7. Call .init_font_character_mapping() at program start to load the correct character
  8. dictionary according to the operating system. Characters (circle sizes) are optimised
  9. to look best for rendering CustomTkinter shapes on the different operating systems.
  10. - .create_aa_circle() creates antialiased circle and returns int identifier.
  11. - .coords() is modified to support the aa-circle shapes correctly like you would expect.
  12. - .itemconfig() is also modified to support aa-cricle shapes.
  13. The aa-circles are created by choosing a character from the custom created and loaded
  14. font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling
  15. either the whole character space or just pert of it (characters A to R). Circles with a smaller
  16. radius need a smaller circle character to look correct when rendered on the canvas.
  17. For an optimal result, the draw-engine creates two aa-circles on top of each other, while
  18. one is rotated by 90 degrees. This helps to make the circle look more symetric, which is
  19. not can be a problem when using only a single circle character.
  20. """
  21. radius_to_char_fine: dict = None # dict to map radius to font circle character
  22. def __init__(self, *args, **kwargs):
  23. super().__init__(*args, **kwargs)
  24. self._aa_circle_canvas_ids = set()
  25. @classmethod
  26. def init_font_character_mapping(cls):
  27. """ optimizations made for Windows 10, 11 only """
  28. radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B',
  29. 10: 'B',
  30. 9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
  31. radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
  32. 11: 'C', 10: 'C',
  33. 9: 'D', 8: 'D', 7: 'D', 6: 'C', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
  34. 0: 'A'}
  35. radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
  36. 11: 'D', 10: 'D',
  37. 9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R',
  38. 0: 'A'}
  39. radius_to_char_fine_linux = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'F', 12: 'C',
  40. 11: 'F', 10: 'C',
  41. 9: 'D', 8: 'G', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'M', 2: 'H', 1: 'H',
  42. 0: 'A'}
  43. if sys.platform.startswith("win"):
  44. if sys.getwindowsversion().build > 20000: # Windows 11
  45. cls.radius_to_char_fine = radius_to_char_fine_windows_11
  46. else: # < Windows 11
  47. cls.radius_to_char_fine = radius_to_char_fine_windows_10
  48. elif sys.platform.startswith("linux"): # Optimized on Kali Linux
  49. cls.radius_to_char_fine = radius_to_char_fine_linux
  50. else:
  51. cls.radius_to_char_fine = radius_to_char_fine_windows_10
  52. def _get_char_from_radius(self, radius: int) -> str:
  53. if radius >= 20:
  54. return "A"
  55. else:
  56. return self.radius_to_char_fine[radius]
  57. def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
  58. tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
  59. # create a circle with a font element
  60. circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill,
  61. font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
  62. self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
  63. self._aa_circle_canvas_ids.add(circle_1)
  64. return circle_1
  65. def coords(self, tag_or_id, *args):
  66. if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id):
  67. coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag
  68. super().coords(coords_id, *args[:2])
  69. if len(args) == 3:
  70. super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
  71. elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
  72. super().coords(tag_or_id, *args[:2])
  73. if len(args) == 3:
  74. super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
  75. else:
  76. super().coords(tag_or_id, *args)
  77. def itemconfig(self, tag_or_id, *args, **kwargs):
  78. kwargs_except_outline = kwargs.copy()
  79. if "outline" in kwargs_except_outline:
  80. del kwargs_except_outline["outline"]
  81. if type(tag_or_id) == int:
  82. if tag_or_id in self._aa_circle_canvas_ids:
  83. super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
  84. else:
  85. super().itemconfigure(tag_or_id, *args, **kwargs)
  86. else:
  87. configure_ids = self.find_withtag(tag_or_id)
  88. for configure_id in configure_ids:
  89. if configure_id in self._aa_circle_canvas_ids:
  90. super().itemconfigure(configure_id, *args, **kwargs_except_outline)
  91. else:
  92. super().itemconfigure(configure_id, *args, **kwargs)