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())
|