ctk_image.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. from typing import Tuple, Dict, Callable, List
  2. try:
  3. from PIL import Image, ImageTk
  4. except ImportError:
  5. pass
  6. class CTkImage:
  7. """
  8. Class to store one or two PIl.Image.Image objects and display size independent of scaling:
  9. light_image: PIL.Image.Image for light mode
  10. dark_image: PIL.Image.Image for dark mode
  11. size: tuple (<width>, <height>) with display size for both images
  12. One of the two images can be None and will be replaced by the other image.
  13. """
  14. _checked_PIL_import = False
  15. def __init__(self,
  16. light_image: "Image.Image" = None,
  17. dark_image: "Image.Image" = None,
  18. size: Tuple[int, int] = (20, 20)):
  19. if not self._checked_PIL_import:
  20. self._check_pil_import()
  21. self._light_image = light_image
  22. self._dark_image = dark_image
  23. self._check_images()
  24. self._size = size
  25. self._configure_callback_list: List[Callable] = []
  26. self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
  27. self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
  28. @classmethod
  29. def _check_pil_import(cls):
  30. try:
  31. _, _ = Image, ImageTk
  32. except NameError:
  33. raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported")
  34. def add_configure_callback(self, callback: Callable):
  35. """ add function, that gets called when image got configured """
  36. self._configure_callback_list.append(callback)
  37. def remove_configure_callback(self, callback: Callable):
  38. """ remove function, that gets called when image got configured """
  39. self._configure_callback_list.remove(callback)
  40. def configure(self, **kwargs):
  41. if "light_image" in kwargs:
  42. self._light_image = kwargs.pop("light_image")
  43. self._scaled_light_photo_images = {}
  44. self._check_images()
  45. if "dark_image" in kwargs:
  46. self._dark_image = kwargs.pop("dark_image")
  47. self._scaled_dark_photo_images = {}
  48. self._check_images()
  49. if "size" in kwargs:
  50. self._size = kwargs.pop("size")
  51. # call all functions registered with add_configure_callback()
  52. for callback in self._configure_callback_list:
  53. callback()
  54. def cget(self, attribute_name: str) -> any:
  55. if attribute_name == "light_image":
  56. return self._light_image
  57. if attribute_name == "dark_image":
  58. return self._dark_image
  59. if attribute_name == "size":
  60. return self._size
  61. def _check_images(self):
  62. # check types
  63. if self._light_image is not None and not isinstance(self._light_image, Image.Image):
  64. raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}")
  65. if self._dark_image is not None and not isinstance(self._dark_image, Image.Image):
  66. raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}")
  67. # check values
  68. if self._light_image is None and self._dark_image is None:
  69. raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.")
  70. # check sizes
  71. if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size:
  72. raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.")
  73. def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]:
  74. return round(self._size[0] * widget_scaling), round(self._size[1] * widget_scaling)
  75. def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage":
  76. if scaled_size in self._scaled_light_photo_images:
  77. return self._scaled_light_photo_images[scaled_size]
  78. else:
  79. self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size))
  80. return self._scaled_light_photo_images[scaled_size]
  81. def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> "ImageTk.PhotoImage":
  82. if scaled_size in self._scaled_dark_photo_images:
  83. return self._scaled_dark_photo_images[scaled_size]
  84. else:
  85. self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size))
  86. return self._scaled_dark_photo_images[scaled_size]
  87. def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: str) -> "ImageTk.PhotoImage":
  88. scaled_size = self._get_scaled_size(widget_scaling)
  89. if appearance_mode == "light" and self._light_image is not None:
  90. return self._get_scaled_light_photo_image(scaled_size)
  91. elif appearance_mode == "light" and self._light_image is None:
  92. return self._get_scaled_dark_photo_image(scaled_size)
  93. elif appearance_mode == "dark" and self._dark_image is not None:
  94. return self._get_scaled_dark_photo_image(scaled_size)
  95. elif appearance_mode == "dark" and self._dark_image is None:
  96. return self._get_scaled_light_photo_image(scaled_size)