Advertisement
Guest User

Dynamic Widget System With Persistence

a guest
Apr 21st, 2025
105
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.44 KB | None | 0 0
  1. import dearpygui.dearpygui as dpg
  2. import os
  3. import json
  4. import uuid
  5. import logging # Added for better logging
  6.  
  7. # --- Logging Setup ---
  8. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  9.  
  10. INI_PATH = "test_layout.ini"
  11. WIDGET_DATA_PATH = "test_widgets.json"
  12.  
  13. # --- Widget Classes ---
  14. class SimpleWidget:
  15. """A basic widget example that can be dynamically created and managed."""
  16. def __init__(self, instance_id: str, config: dict, layout_manager):
  17. self.instance_id = instance_id
  18. self.config = config
  19. self.layout_manager = layout_manager
  20. self.window_tag = f"widget_win_{self.instance_id}"
  21.  
  22. def create(self):
  23. """Creates the DPG window for this widget."""
  24. if dpg.does_item_exist(self.window_tag):
  25. logging.warning(f"Window {self.window_tag} already exists. Skipping creation.")
  26. return
  27. logging.info(f"Creating DPG window for widget {self.instance_id} with tag {self.window_tag}")
  28. with dpg.window(
  29. label=self.config.get("label", f"Widget {self.instance_id[:8]}"),
  30. tag=self.window_tag,
  31. on_close=self._on_window_close,
  32. width=200, # Default size
  33. height=150 # Default size
  34. ):
  35. dpg.add_text(f"ID: {self.instance_id}")
  36. dpg.add_text(f"Label: {self.config.get('label', 'N/A')}")
  37. # Add more content based on config if needed
  38. dpg.add_input_text(label="Config Value", default_value=self.config.get("value", ""), callback=self._on_value_change)
  39.  
  40. def get_config(self) -> dict:
  41. """Returns the current configuration of the widget."""
  42. return self.config
  43.  
  44. def _on_window_close(self, sender, app_data, user_data):
  45. """Callback when the DPG window is closed."""
  46. logging.info(f"DPG window closed for widget: {self.instance_id} (Tag: {self.window_tag})")
  47. self.layout_manager.remove_widget(self.instance_id)
  48. # Note: DPG handles deleting the window item itself when 'on_close' is triggered
  49. # and the window is actually closed by the user. We just need to handle our internal state.
  50.  
  51. def _on_value_change(self, sender, app_data, user_data):
  52. """Callback to update the config when the input text changes."""
  53. self.config["value"] = app_data
  54. logging.debug(f"Widget {self.instance_id} value updated: {app_data}") # Use debug level for frequent updates
  55.  
  56. # --- Layout Manager ---
  57. class LayoutManager:
  58. """Manages the lifecycle and persistence of dynamic widgets."""
  59. def __init__(self):
  60. self.active_widgets = {}
  61. # Map widget type names (strings) to their actual classes
  62. self.widget_classes = {
  63. "SimpleWidget": SimpleWidget
  64. # Add other widget types here if needed
  65. }
  66. self._next_widget_index = 0 # Simple counter for default labels
  67.  
  68. def add_widget(self, instance_id: str = None, widget_type: str = "SimpleWidget", config: dict = None):
  69. """Creates, registers, and displays a new widget instance."""
  70. if widget_type not in self.widget_classes:
  71. logging.error(f"Error: Unknown widget type '{widget_type}'")
  72. return None
  73.  
  74. if instance_id is None:
  75. instance_id = uuid.uuid4().hex
  76.  
  77. if instance_id in self.active_widgets:
  78. logging.warning(f"Widget with ID {instance_id} already exists.")
  79. # Optionally, could focus the existing window here
  80. return self.active_widgets[instance_id]
  81.  
  82. if config is None:
  83. self._next_widget_index += 1
  84. config = {"label": f"Widget {self._next_widget_index}", "value": f"Default {self._next_widget_index}"}
  85.  
  86. WidgetClass = self.widget_classes[widget_type]
  87. widget_instance = WidgetClass(instance_id, config, self)
  88.  
  89. logging.info(f"Adding widget: {instance_id} (Type: {widget_type}, Config: {config})")
  90. self.active_widgets[instance_id] = widget_instance
  91. widget_instance.create() # Create the DPG window
  92. return widget_instance
  93.  
  94. def remove_widget(self, instance_id: str):
  95. """Removes a widget instance from active tracking."""
  96. if instance_id in self.active_widgets:
  97. logging.info(f"Removing widget {instance_id} from layout manager tracking.")
  98. del self.active_widgets[instance_id]
  99. # Note: The corresponding dpg.window should be deleted via its on_close callback
  100. # or explicitly if removed programmatically without closing the window.
  101. else:
  102. logging.warning(f"Attempted to remove non-existent widget ID: {instance_id}")
  103.  
  104. def save_layout(self, ini_path: str, json_path: str):
  105. """Saves the DPG window layout (.ini) and widget configurations (.json)."""
  106. logging.info(f"Starting layout save to {ini_path} and {json_path}")
  107.  
  108. # Log current state *before* saving INI
  109. logging.info("Current DPG state of active widgets before saving:")
  110. if not self.active_widgets:
  111. logging.info(" -> No active widgets to log state for.")
  112. for instance_id, widget in self.active_widgets.items():
  113. tag = widget.window_tag
  114. if dpg.does_item_exist(tag):
  115. config = dpg.get_item_configuration(tag)
  116. state = dpg.get_item_state(tag)
  117. logging.info(f" Widget ID: {instance_id} | Tag: {tag}")
  118. logging.info(f" Config: Pos={config.get('pos')}, Size={config.get('width')},{config.get('height')}, Label='{config.get('label')}'")
  119. logging.info(f" State: Visible={state.get('visible')}, Docked={state.get('docked')}") # Added docked state
  120. else:
  121. logging.warning(f" Widget ID: {instance_id} | Tag: {tag} does not exist in DPG at save time.")
  122.  
  123. # 1. Save DPG window states (position, size, docking, etc.)
  124. try:
  125. dpg.save_init_file(ini_path)
  126. logging.info(f" -> Successfully saved DPG state to {ini_path}")
  127. except Exception as e:
  128. logging.error(f" -> Error saving DPG state to {ini_path}: {e}")
  129. # Decide if we should proceed with saving JSON if INI save fails
  130. # For now, we'll continue
  131.  
  132. # 2. Prepare widget data for saving
  133. widget_data = []
  134. for instance_id, widget in self.active_widgets.items():
  135. widget_data.append({
  136. "instance_id": instance_id,
  137. "widget_type": type(widget).__name__,
  138. "config": widget.get_config() # Get potentially updated config
  139. })
  140.  
  141. # 3. Save widget data to JSON
  142. try:
  143. with open(json_path, 'w') as f:
  144. json.dump(widget_data, f, indent=4)
  145. logging.info(f" -> Successfully saved widget configurations to {json_path}")
  146. except Exception as e:
  147. logging.error(f"Error saving widget data to {json_path}: {e}")
  148.  
  149. logging.info(f"Layout saving finished.")
  150.  
  151. def load_layout(self, ini_path: str, json_path: str):
  152. """Loads widget configurations (.json) and applies DPG layout (.ini)."""
  153. logging.info(f"Starting layout load from {ini_path} and {json_path}")
  154. widget_data = []
  155. # 1. Load widget configurations from JSON
  156. if os.path.exists(json_path):
  157. try:
  158. with open(json_path, 'r') as f:
  159. widget_data = json.load(f)
  160. logging.info(f" -> Successfully loaded {len(widget_data)} widget configurations from {json_path}")
  161. except Exception as e:
  162. logging.error(f"Error loading widget data from {json_path}: {e}")
  163. widget_data = [] # Reset on error
  164. else:
  165. logging.info(f" -> Widget configuration file {json_path} not found. Starting fresh.")
  166.  
  167. # 2. Recreate widget instances and their DPG windows *before* loading INI
  168. highest_index = 0
  169. recreated_widget_tags = []
  170. if widget_data: # Check if list is not empty
  171. logging.info(f" -> Recreating {len(widget_data)} widgets based on loaded data...")
  172. for data in widget_data:
  173. instance_id = data.get("instance_id")
  174. widget_type = data.get("widget_type")
  175. config = data.get("config", {})
  176.  
  177. if not instance_id or not widget_type:
  178. logging.warning(f"Warning: Skipping invalid widget data entry: {data}")
  179. continue
  180.  
  181. # Use a local variable to avoid modifying the method signature for logging
  182. _widget_instance = self.add_widget(instance_id, widget_type, config)
  183. if _widget_instance:
  184. recreated_widget_tags.append(_widget_instance.window_tag)
  185. # Try to find the highest index from loaded labels for the counter
  186. try:
  187. label_parts = config.get("label", "").split()
  188. if len(label_parts) > 1 and label_parts[0] == "Widget":
  189. index = int(label_parts[-1])
  190. highest_index = max(highest_index, index)
  191. except ValueError:
  192. pass # Ignore labels not matching the pattern
  193. self._next_widget_index = highest_index # Update counter
  194. logging.info(f" -> Finished recreating widgets. Next index: {self._next_widget_index + 1}")
  195. else:
  196. logging.info(" -> No previous widget data found or loaded.")
  197. self._next_widget_index = 0
  198.  
  199. # 2.5 Log state *after* creation, *before* applying INI
  200. logging.info(f"State of {len(recreated_widget_tags)} recreated widgets BEFORE applying INI:")
  201. if not recreated_widget_tags:
  202. logging.info(" -> No widgets were recreated.")
  203. for tag in recreated_widget_tags:
  204. if dpg.does_item_exist(tag):
  205. config = dpg.get_item_configuration(tag)
  206. state = dpg.get_item_state(tag)
  207. logging.info(f" Tag: {tag}")
  208. logging.info(f" Config: Pos={config.get('pos')}, Size={config.get('width')},{config.get('height')}")
  209. logging.info(f" State: Visible={state.get('visible')}, Docked={state.get('docked')}")
  210. else:
  211. logging.warning(f" Tag: {tag} (expected to exist) does not exist BEFORE applying INI.")
  212.  
  213. # 3. Apply DPG layout from INI *after* windows are created
  214. ini_applied = False
  215. if os.path.exists(ini_path):
  216. logging.info(f" -> Applying DPG layout from {ini_path}...")
  217. try:
  218. # Use configure_app which is safer than load_init_file during runtime setup
  219. dpg.configure_app(init_file=ini_path)
  220. logging.info(f" -> Successfully applied DPG layout from {ini_path}")
  221. ini_applied = True
  222. except Exception as e:
  223. logging.error(f" -> Error applying DPG layout from {ini_path}: {e}")
  224. else:
  225. logging.info(f" -> DPG layout file {ini_path} not found. Using default layout.")
  226.  
  227. # 3.5 Log state *after* applying INI
  228. logging.info(f"State of {len(recreated_widget_tags)} recreated widgets AFTER applying INI (Applied={ini_applied}):")
  229. if not recreated_widget_tags:
  230. logging.info(" -> No widgets were recreated to check state.")
  231. for tag in recreated_widget_tags:
  232. if dpg.does_item_exist(tag):
  233. config = dpg.get_item_configuration(tag)
  234. state = dpg.get_item_state(tag)
  235. logging.info(f" Tag: {tag}")
  236. logging.info(f" Config: Pos={config.get('pos')}, Size={config.get('width')},{config.get('height')}")
  237. logging.info(f" State: Visible={state.get('visible')}, Docked={state.get('docked')}")
  238. else:
  239. # This might happen if the INI file somehow removes a window, though unlikely here
  240. logging.warning(f" Tag: {tag} does not exist AFTER applying INI.")
  241.  
  242.  
  243. logging.info("Layout loading finished.")
  244.  
  245.  
  246. # --- Main Application Logic ---
  247. def main():
  248. dpg.create_context()
  249. logging.info("DPG Context Created.")
  250.  
  251. layout_manager = LayoutManager()
  252.  
  253. # --- Callbacks ---
  254. def _add_new_widget_callback():
  255. logging.info("'Add SimpleWidget' button clicked")
  256. layout_manager.add_widget()
  257.  
  258. def _save_layout_callback():
  259. logging.info("'Save Layout' button clicked")
  260. layout_manager.save_layout(INI_PATH, WIDGET_DATA_PATH)
  261.  
  262. # --- Control Window (Create BEFORE load so it exists if INI references it) ---
  263. # Note: We create it here, but its state (pos/size) will be potentially
  264. # overridden by load_layout if it was saved in the INI.
  265. logging.info("Creating control window.")
  266. with dpg.window(label="Controls", tag="control_window", width=300, height=100):
  267. # Start hidden, rely on INI or default DPG placement to show it
  268. dpg.add_button(label="Add SimpleWidget", callback=_add_new_widget_callback)
  269. dpg.add_button(label="Save Layout", callback=_save_layout_callback)
  270.  
  271. # --- DPG Setup (Viewport and main setup) ---
  272. logging.info("Setting up DPG viewport...")
  273. dpg.create_viewport(title='Dynamic Layout Test', width=1280, height=720)
  274.  
  275. # Enable docking
  276. logging.info("Enabling Docking...")
  277. dpg.configure_app(docking=True, docking_space=True)
  278.  
  279. # setup_dearpygui must be called after creating the viewport and before showing it.
  280. dpg.setup_dearpygui()
  281. logging.info("DPG setup complete.")
  282.  
  283. # --- Initial Layout Load ---
  284. # Load layout *after* setup_dearpygui but *before* showing the viewport
  285. # or starting the render loop. This seems like a more robust point.
  286. layout_manager.load_layout(INI_PATH, WIDGET_DATA_PATH)
  287.  
  288. # Now show the viewport *after* the INI might have configured it (including visibility)
  289. dpg.show_viewport()
  290. logging.info("Viewport shown.")
  291.  
  292.  
  293. logging.info("Starting Dear PyGui render loop...")
  294. # Use the manual render loop as requested
  295. while dpg.is_dearpygui_running():
  296. # Insert per-frame logic here if needed
  297. dpg.render_dearpygui_frame()
  298.  
  299. logging.info("Dear PyGui render loop stopped. Cleaning up...")
  300. dpg.destroy_context()
  301. logging.info("DPG Context Destroyed. Cleanup complete.")
  302.  
  303.  
  304. if __name__ == "__main__":
  305. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement