Custom Widgets#
As well as the collection of built-in widget types provided by pyMHF, it is possible to create completely custom widgets which can be bound to mods. Doing so is reasonably complicated, but it provides a lot of power, so may be worth the effort!
Creating a custom widget#
To create a custom widget, you need to create a class which inherits CustomWidget.
This class is an Abstract base class <https://docs.python.org/3/library/abc.html>_, meaning is has a few methods which must be defined on the subclass before python will let it be instantiated.
Required methods#
draw(self)This is the main draw command which will be called once when the widget it to be drawn for the first time. This should create any DearPyGUI widgets which are to be drawn as part of this, as well as creating any variables and binding any callbacks. Almost any DearPyGUI commands relating to creating widgets should be able to be put here (within reason!). It is generally recommended that any widgets which are created here have their DearPyGUI ids stored in the
self.idsdictionary associated with the class as they will get cleaned up by default if the widget is removed.
redraw(self, **kwargs) -> dict[str, Any]:Redraw the widget.
This will be called each frame but is guaranteed not to be called before the original
drawmethod.This method can be defined with any number of arguments but the function decorated by this class MUST return a dict which doesn’t contain any keys which aren’t function arguments. If this decorated a property which also has a setter, the returned dictionary (if any) is passed into that setter by value (ie. it’s not spread out - the values must be unpacked from the dictionary inside the setter).
See the example below on how these two methods work and can be implemented.
Widget behaviour#
To make handling the placement of custom widgets as simple as possible, pyMHF defines a few types of “widget behaviours” which a custom widget may have.
These behaviours are either “continuous” or “separate”
- Continuous:
The widget will be rendered in a row in the existing table which widgets are placed in if possible.
This table consists of 2 columns, so if you custom widget would nicely fit into a 2 column format (eg. label in left column, entry field in right), then it’s recommended that you use this behaviour to make the UI look more seamless.
- Separate:
For a “separate” widget, it will force any existing table to be ended and create a new group for the custom widget to be rendered in. This gives you complete flexibility in what you want to render, however you will need to control the widget of the widget yourself as it will not automatically fill the space horizontally.
This is due to drawlists in DearPyGUI not expanding automatically and breaking if you put them in a group of width -1 which would normally allow a widget to expand to take up the full width.
We define the widget behaviour at the top of the custom widget implementation as follows:
from pymhf.gui.widgets import CustomWidget, WidgetBehaviour
class MyWidget(CustomWidget):
widget_behaviour = WidgetBehaviour.SEPARATE
Example#
The following example has some extra complexity in handling the click events on the drawlist due to some limitations of DearPyGUI.
1import math
2
3import dearpygui.dearpygui as dpg
4
5from pymhf import Mod
6from pymhf.gui.decorators import FLOAT
7from pymhf.gui.widgets import CustomWidget, WidgetBehaviour
8
9
10class MovingCircle(CustomWidget):
11 # Specify that the widget will be drawn separately from the previous widget.
12 widget_behaviour = WidgetBehaviour.SEPARATE
13
14 def __init__(self, colour: tuple[int, int, int, int] = (255, 0, 0, 255)):
15 super().__init__()
16 self.colour = colour
17 self.center_pos = (200, 200)
18 self.clicked_on = False
19
20 def click_callback(self, sender, app_data, user_data):
21 self.clicked_on = True
22
23 def release_mouse(self, sender, app_data, user_data):
24 self.clicked_on = False
25
26 def draw(self):
27 # Create a DearPyGUI drawlist to draw a circle which follows the mouse when clicked.
28 # This code is called when the widget is initially drawn in the GUI and when the
29 # widget is reloded after the "reload" button is pressed.
30 with dpg.drawlist(width=500, height=300) as dl:
31 self.ids["DRAWLIST"] = dl
32 self.ids["BORDER"] = dpg.draw_rectangle(
33 pmin=(0, 0),
34 pmax=(500, 300),
35 color=(255, 255, 255, 255),
36 fill=(0, 0, 0, 0),
37 thickness=1,
38 )
39 self.ids["DOT"] = dpg.draw_circle(
40 center=self.center_pos,
41 radius=2,
42 color=self.colour,
43 fill=self.colour,
44 )
45 self.ids["CIRCLE"] = dpg.draw_circle(
46 center=(self.center_pos[0] + 100, self.center_pos[1] + 100),
47 radius=20,
48 color=self.colour,
49 fill=self.colour,
50 )
51 dpg.add_text("Change the value for theta and \nradius below to move the circle.")
52
53 with dpg.item_handler_registry() as ihr:
54 # Triggers for the left mouse button clicked event within the drawlist bounds
55 dpg.add_item_clicked_handler(
56 button=dpg.mvMouseButton_Left,
57 callback=self.click_callback,
58 )
59
60 dpg.bind_item_handler_registry(self.ids["DRAWLIST"], ihr)
61
62 with dpg.handler_registry():
63 dpg.add_mouse_release_handler(callback=self.release_mouse)
64
65 def redraw(self, theta: float, radius: float, center_pos: tuple[float, float]):
66 # This function is called each frame the tab the widget belongs to is selected.
67 if self.clicked_on:
68 self.center_pos = tuple(dpg.get_drawing_mouse_pos())
69 elif center_pos:
70 self.center_pos = center_pos
71 x = self.center_pos[0] + 50 * math.cos(theta)
72 y = self.center_pos[1] + 50 * math.sin(theta)
73
74 # Update the circle's position using configure_item
75 dpg.configure_item(self.ids["DOT"], center=self.center_pos)
76 dpg.configure_item(self.ids["CIRCLE"], center=(x, y), radius=radius)
77 return {"center_pos": self.center_pos}
78
79
80class GUITest(Mod):
81 __author__ = "monkeyman192"
82 __description__ = "Test globals"
83
84 def __init__(self):
85 super().__init__()
86 self._theta = 0
87 self._radius = 10
88 self.center_pos = (200, 200)
89
90 @property
91 @MovingCircle((255, 0, 123, 255))
92 def circle_loc(self):
93 return {
94 "theta": self._theta,
95 "radius": self._radius,
96 "center_pos": self.center_pos
97 }
98
99 @circle_loc.setter
100 def circle_loc(self, value):
101 self.center_pos = value["center_pos"]
102
103 @property
104 @FLOAT("Theta", is_slider=True, min_value=0, max_value=2 * math.pi)
105 def theta(self):
106 return self._theta
107
108 @theta.setter
109 def theta(self, value):
110 self._theta = value
111
112 @property
113 @FLOAT("Radius", is_slider=True, min_value=0, max_value=50)
114 def radius(self):
115 return self._radius
116
117 @radius.setter
118 def radius(self, value):
119 self._radius = value
The above code will produce a gui like the following:
If we click on anywhere within the border the smaller dot will follow the location of the mouse, and if we drag the theta or radius sliders, the location and size of the larger dot will change.
Let’s break down the above code to see what is going on to get a better idea of what is happening.
We create the MovingCircle class which inherits from CustomWidget