In this blog post, we’ll dive into the enhancements made to the custom scripting language and its interpreter built using Python and PyQt6. If you have not read it yet, click here(Building a Custom Scripting Language in PyQt6 for Dynamic Widget Rendering) This project allows users to dynamically generate UI components based on a simple, user-friendly script. The focus is on key improvements such as the command execution, clearing the right pane before displaying new widgets, saving widgets into a registry for better control, and additional functionality like widget alignment and action handling.
Let's walk through the enhancements and improvements in this project:
Key Enhancements
1. Clearing the Right Pane Before Displaying New Widgets
Before the interpreter renders new widgets based on the script, we ensure the right pane is cleared. This prevents residual widgets from previous script executions from cluttering the view.
1 2 3 4 5 6 | # Remove all widgets from the layout while layout.count(): item = layout.takeAt(0) # Get the first item from the layout widget = item.widget() # Get the widget from the layout item if widget is not None: widget.setParent(None) # Remove the widget from the layout |
This block of code ensures that the right pane starts fresh every time a new script is run, improving the user experience and maintaining a clean interface.
2. Action Handling When Buttons Are Clicked
One of the most significant enhancements is adding the ability to define actions within a script. This is accomplished through the use of Command
and End Command
blocks. The actions are linked to widgets, primarily buttons, allowing for dynamic behavior based on user interaction.
1 2 3 4 | # Command for button click Command: Button_Clicked:Submit:print Text1:Label2 End Command |
The above block of code defines that when the "Submit" button is clicked, it prints the content of Text1
and updates the text in Label2
. This flexibility opens up many possibilities, such as form handling, real-time updates, and interactive elements in the generated UI.
3. Saving All Widgets in a Widget Registry
To maintain control over the dynamically created widgets, we store them in a registry. This allows for easy access, modification, and action application based on widget names provided in the script.
1 | self.widget_registry = {} # Store references to widgets by name |
Each widget (e.g., buttons, labels, text inputs) is stored in this registry when created. This makes it easy to retrieve them later when executing commands like updating text or handling button clicks.
For example, here's how we register a button:
1 2 | button_name = label.strip() self.widget_registry[button_name] = button # Register button by name |
4. Support for Widget Alignment
Labels in the script now support text alignment, allowing the user to specify whether the label’s text should be left, center, or right aligned. This provides more control over the layout of the generated UI.
1 | label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center alignment |
The script syntax supports this enhancement:
1 | Label Label2(Result:Center) |
This creates a label with the text "Result" centered in its container.
5. Dynamic Command Parsing
The custom scripting language includes commands like Button_Clicked
, which are dynamically parsed and executed. Here’s how it works:
1 2 3 4 5 6 7 8 9 10 11 | def _parse_command(self, line): if line.startswith("Button_Clicked"): parts = line.split(':') button_name = parts[1] action = parts[2] target = parts[3] self.commands.append({ "button_name": button_name, "action": action, "target": target }) |
This method parses the button click command and stores it, allowing for delayed execution when the button is actually clicked. The action is bound using partial
, capturing both the action and the target widget to apply the changes when the button is clicked.
1 | button.clicked.connect(partial(self._execute_action, action, target)) |
6. Command Execution Based on Script Instructions
When a button is clicked, the associated command is executed. For example, the action to print the content of a text input is defined and applied dynamically:
1 2 3 4 5 6 7 8 9 | def _execute_action(self, action, target): if action.startswith("print"): widget = self.widget_registry.get(target_widget) if isinstance(widget, QLineEdit): print(widget.text()) if target: widget1 = self.widget_registry.get(target) if isinstance(widget1, QLabel): widget1.setText(widget.text()) |
In this example, the Submit
button prints the content of a QLineEdit
and updates a QLabel
with the same text. This demonstrates the ability to build interactive forms and real-time updates within the custom UI.
Sample Script
Here’s an example script demonstrating all of the new features:
1 2 3 4 5 6 7 8 | Label Label1(Enter text)
TextInput Text1(Type text here)
Buttons Submit
Label Label2(Result:Center)
Command:
Button_Clicked:Submit:print Text1:Label2
End Command
|
And here is the rendered screen:
This script creates a simple form with a text input and a submit button. Upon clicking the "Submit" button, the text from Text1
is printed and displayed in Label2
, which is centered.
Conclusion
By expanding the custom script interpreter, we've added significant flexibility and functionality. Users can now define actions, align labels, and work with a cleaner interface thanks to automatic widget clearing. The widget registry ensures full control over the dynamically created UI elements, making this tool powerful for building custom user interfaces without needing to write traditional PyQt code.
These enhancements pave the way for building more complex applications with minimal effort, offering a higher level of abstraction for UI development. Whether you’re designing forms, dashboards, or simple tools, this script-based approach makes development faster and more intuitive.
And here is the complete program listing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | import sys from PyQt6.QtWidgets import ( QApplication, QMainWindow, QSplitter, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QScrollArea, QLabel, QLineEdit, QSpacerItem, QSizePolicy ) from PyQt6.QtCore import Qt from functools import partial import pdb class ScriptInterpreter: def __init__(self, script, preview_container): self.script = script self.preview_container = preview_container self.widget_registry = {} # Store references to widgets by name self.commands = [] # Store commands to be executed def interpret(self): # Clear previous widgets layout = self.preview_container.layout() if layout: for i in reversed(range(layout.count())): widget_to_remove = layout.itemAt(i).widget() if widget_to_remove is not None: widget_to_remove.setParent(None) end_horizontal_found = True in_command_block = False # Parse the script line by line lines = self.script.splitlines() for line in lines: line = line.strip() if line.startswith("Buttons") and end_horizontal_found == True: self._create_button(line) elif line.startswith("Horizontal:"): # Start a new horizontal layout horizontal_layout = QHBoxLayout() horizontal_layout.setSpacing(10) self._add_hbox_layout_to_container(horizontal_layout) end_horizontal_found = False elif line.startswith("Buttons") and end_horizontal_found == False: self._create_button(line) elif line.startswith("Label") and end_horizontal_found == False: self._create_label(line, horizontal_layout) elif line.startswith("TextInput") and end_horizontal_found == False: self._create_text_input(line, horizontal_layout) elif line.startswith("Label") and end_horizontal_found == True: self._create_label(line) elif line.startswith("TextInput") and end_horizontal_found == True: self._create_text_input(line) elif line.startswith("End Horizontal"): end_horizontal_found = True # Check for Command blocks elif line.startswith("Command:"): #pdb.set_trace() in_command_block = True continue elif line.startswith("End Command"): in_command_block = False self._apply_commands() continue elif in_command_block == True : self._parse_command(line) continue # Add spacer to push everything to the top spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) layout.addItem(spacer) def _parse_command(self, line): """ Parse commands from the Command block. """ #pdb.set_trace() if line.startswith("Button_Clicked"): parts = line.split(':') button_name = parts[1] action = parts[2] target = parts[3] self.commands.append({ "button_name": button_name, "action": action, "target": target }) def _apply_commands(self): #pdb.set_trace() """ Apply the parsed commands to the corresponding widgets. """ for command in self.commands: button_name = command["button_name"] action = command["action"] target = command["target"] action = action.strip() button_name= button_name.strip() target = target.strip() # Retrieve the button from the registry button = self.widget_registry.get(button_name.strip()) if button is not None: # Use lambda to capture the action argument properly #button.clicked.connect(lambda _, a=action: self._execute_action(a)) button.clicked.connect(partial(self._execute_action, action, target)) def _execute_action(self, action, target): #pdb.set_trace() """ Execute the action when the button is clicked. """ if action.startswith("print"): # Extract the target of the print command _, target_widget = action.split(maxsplit=1) widget = self.widget_registry.get(target_widget) if isinstance(widget, QLineEdit): # Print the content of the text input print(widget.text()) if target: widget1 = self.widget_registry.get(target) if isinstance(widget1, QLabel): widget1.setText(widget.text()) def _create_button(self, line, layout=None): _, label = line.split(maxsplit=1) button = QPushButton(label) button.setFixedHeight(40) # Set fixed height button_name = label.strip() self.widget_registry[button_name] = button # Register button by name if layout != None: layout.addWidget(button) else: self.preview_container.layout().addWidget(button) def _create_label(self, line, layout=None): # Extract label information and alignment label_parts = line.split("(") label_name = label_parts[0].split()[1].strip() # Label name (like Label1) if len(label_parts) > 1: text_and_align = label_parts[1].rstrip(")").split(":") label_text = text_and_align[0].strip() # Label text (like "Result") alignment = text_and_align[1].strip().lower() if len(text_and_align) > 1 else 'left' else: label_text = "" alignment = 'left' label = QLabel(label_text) label.setFixedHeight(40) # Set fixed height # Register the text input by its name self.widget_registry[label_name] = label # Set alignment based on the provided value (left, center, right) if alignment == 'center': label.setAlignment(Qt.AlignmentFlag.AlignCenter) elif alignment == 'right': label.setAlignment(Qt.AlignmentFlag.AlignRight) else: label.setAlignment(Qt.AlignmentFlag.AlignLeft) if layout != None: layout.addWidget(label) else: self.preview_container.layout().addWidget(label) def _create_text_input(self, line, layout=None): # Extract the name and placeholder from the line name_placeholder = line[len("TextInput "):] # Remove "TextInput " part name, placeholder = name_placeholder.split("(", 1) name = name.strip() placeholder = placeholder.rstrip(")") # Remove closing parenthesis text_input = QLineEdit() text_input.setPlaceholderText(placeholder) text_input.setFixedHeight(40) # Set fixed height # Register the text input by its name self.widget_registry[name] = text_input if layout != None: layout.addWidget(text_input) else: self.preview_container.layout().addWidget(text_input) def _add_hbox_layout_to_container(self, layout): # Create a container widget for the HBoxLayout hbox_widget = QWidget() hbox_widget.setLayout(layout) #hbox_widget.setFixedHeight(50) # Set a fixed height for the HBoxLayout widget self.preview_container.layout().addWidget(hbox_widget) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Custom Scripting Language Interpreter") self.setGeometry(100, 100, 800, 600) # Create a QSplitter to split left and right sections self.splitter = QSplitter(Qt.Orientation.Horizontal, self) # Left Pane: TextEdit for entering script and a button to test it self.left_widget = QWidget() self.left_layout = QVBoxLayout(self.left_widget) self.text_edit = QTextEdit() self.text_edit.setPlaceholderText("Enter your script here...") self.test_button = QPushButton("Test Script") self.test_button.clicked.connect(self.run_script) self.left_layout.addWidget(self.text_edit) self.left_layout.addWidget(self.test_button) # Right Pane: ScrollArea to preview the widgets from the script self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.preview_container = QWidget() self.preview_container.setLayout(QVBoxLayout()) self.scroll_area.setWidget(self.preview_container) # Add left and right panes to the splitter self.splitter.addWidget(self.left_widget) self.splitter.addWidget(self.scroll_area) splitter_sizes = [int(0.30 * self.width()), int(0.70 * self.width())] self.splitter.setSizes(splitter_sizes) # Set splitter as the central widget of the main window self.setCentralWidget(self.splitter) def get_all_widgets(self, scroll_area): # Get the container widget inside the QScrollArea container = scroll_area.widget() # Check if the container has a layout if container.layout() is not None: # Get all widgets from the container widgets = [] layout = container.layout() for i in range(layout.count()): widget = layout.itemAt(i).widget() if widget is not None: widgets.append(widget) return widgets else: return [] def run_script(self): # Get the layout of the container inside the scroll area layout = self.preview_container.layout() # Remove all widgets from the layout while layout.count(): item = layout.takeAt(0) # Get the first item from the layout widget = item.widget() # Get the widget from the layout item if widget is not None: widget.setParent(None) # Remove the widget from the layout # Get the script from the text editor script = self.text_edit.toPlainText() # Interpret and render the widgets in the right pane interpreter = ScriptInterpreter(script, self.preview_container) interpreter.interpret() # Main application app = QApplication(sys.argv) main_window = MainWindow() main_window.show() # Run the PyQt6 event loop sys.exit(app.exec()) |
No comments:
Post a Comment