Working with custom widgets and signals in Maya PyQt

In my previous post, I introduced the fundamentals of using PyQt to develop interfaces for Maya. Now, I’m going to take it a step further and show you how to create custom widgets along with their own custom signals and connections.

Our goal is to build upon the previous tutorial to create a more advanced dialog capable of creating many different poly shapes at once. Our end result should look something like this:

As you can see, we have an Add Poly Shape button so that we can have as many shapes as we want and each line has a Remove button in case we change our minds later. Also, there’s a descriptive line near the top left that tracks the total number of (enabled) shapes that will be created.

In this tutorial, we will

  • Create a custom widget.
  • Emit our own signals from it.
  • Learn how to pass data through those signals.
  • Dynamically add and remove widgets in the UI.
  • Add and tweak layouts for finer control of the UI appearance.

Source Code

The final source code for this tutorial can be downloaded here:
qtCustom

Getting Started

We will begin in much the same manner as the previous tutorial: by laying down some boilerplate code and initializing our main dialog.

Notice that we added a resize command this time. This way, our dialog will always have a consistent starting size. We can test the current state of our dialog at any time by executing:

We continue by adding the buttons and descriptive label:

A Better Layout

Let’s begin by laying out our buttons as we learned previously:

If we do a quick preview:

Hmmm, that’s not quite what we want. The spacing between the first and second row is really big and there’s definitely waaay too much space at the top. Plus, the buttons are kinda wide.

To fix this, we can add stretch to our layouts. Stretch is useful for consuming any excess space. It can be assigned to a widget or simply added by itself. Let’s update our layout with some stretch:

Result:

That’s much better but there’s still some improvements to be made. The Make Shapes button is still stretched and now the space between the first and second line is too small. We want to increase that spacing. Let’s add the following modifications:

We’ve used the setContentsMargins method to explicitly set the outer margins of each of our layouts. The arguments specify the size, in pixels, of each margin in the following order:

  • left
  • top
  • right
  • bottom

We’re also using the setSpacing method to tell the layout how many pixels to separate each of its items by. Our new layout results in this:

The Custom Widget

Now that we’ve got our dialog’s basic layout, let’s leave it for the moment and focus on building a custom widget for adding poly shapes. The code for this is very similar to our code from the previous tutorial except that we derive from QtGui.QWidget rather than QtGui.QDialog. That’s because QWidget is the base class for all widgets.

I’ve provided the initialization code as one big chunk because it is almost identical to what we did for the BasicDialog in the earlier tutorial.

Now, we’ve created our own widget that can be easily added to any UI. To get a quick peek at how the PolyShapeMaker will appear in our dialog, we’ll add the following line to our CustomWidgetDialog layout:

Which results in:

Be mindful that what we just did is a total hack! We only did it to quickly display the resulting widget in the dialog.

Dynamically Adding Widgets

That layout hack makes me feel really dirty! Let’s add a proper addShape method to CustomWidgetDialog. But first, since we’ll be constantly referring back to them, we need to create a variable for tracking all the PolyShapeMakers that are added to CustomWidgetDialog. We’ll do that by updating the __init__ method:

Now, we can define that addShape method:

Notice:

  • When initializing a new PolyShapeMaker instance, we must pass in self (the CustomWidgetDialog instance) as the parent argument.
  • Since we want the new PolyShapeMaker to show up in the layout before the Make Shapes button, we must use the insertWidget method rather than the addWidget method as we’d done previously.

Let’s also get rid of that dirty hack but have the initialization add one shape by default:

If we launch our dialog again, we should see exactly the same result as earlier:

Let’s connect our Add Poly Shape button to the addShape method so that we can see it in action:

Custom Signals

At this point, we’ve got the layout pretty much done. Unfortunately, it’s all flash and no substance as nothing actually works! To fix that, we need to go back to our PolyShapeMaker widget and start fleshing out its connections and signals.

First, a quick briefing on emitting signals with the emit method:

    • Any widget can emit or connect a signal.
    • The signal you emit can be named anything you want.

 

  • You can pass data through that signal.

    Any method connected to the signal can access the data by simply including a slot for that data within its definition.
  • Multiple data values can be passed within a signal

    The corresponding connected function should have as many slots:
  • Since Qt is actually written in C++, the acceptable data types that may be passed via a signal must be defined in C++ NOTPython. Acceptable types include:
    • int
    • bool

    You may also use data types defined by Qt such as:

    • QString
    • QColor
    • QSize
  • The following Python data types may NOTbe passed through a signal:
    • string (use QString instead)
    • dict
    • list

    EXCEPT when using a special PyQt_PyObject data type as we’ll see in Step 2.

Step 1: Emit a signal when the checkbox is toggled:

Step 2: Emit a signal when the remove button is clicked:

What the heck is PyQt_PyObject? This is a special data type that may be placed in a signal when you need to pass a pointer to any Python object. In this case, we’re passing self, which is the current instance of PolyShapeMaker.

Finishing PolyShapeMaker

We’re almost done with our PolyShapeMaker class. Let’s finish it off by defining a couple more methods that may be useful to its parent dialog:

  • isEnabled – for checking whether the widget is actually enabled (i.e., the checkbox is checked).
  • getPolyCmd – for returning the function necessary to build the poly shape defined by this widget.

Counting Shapes

We are now officially done with the PolyShapeMaker implementation! Now seems like a good time to go back to our CustomWidgetDialog and define a method for updating the descCountLabel, which is the descriptive text at the top left that tells the user how many shapes will actually be created:

Removing Widgets

So far, we’ve seen how to dynamically add widgets to our UI. However, we also need to be able to do the opposite so that we can support the Remove button on each widget. The implementation is actually very simple and only requires that we remove the undesirable widget from any list or layout before deleting it.

Notice:

  • We can directly remove a widget from a layout by simply calling layout.removeWidget(widget).
  • It is not enough to remove a widget from the layout as it will still be visible in the UI, albeit out of place. To completely get rid of the widget, we must call its deleteLater method.

Revisiting addShape

Almost done! Let’s go back to our addShape method and add connections for the signals from any new PolyShapeMaker‘s checkbox and Remove button to the appropriate methods in our CustomWidgetDialog:

Notice:

  • The “isEnabledChanged(bool)” signal transmits a single boolean argument to any receiving function. However, it is connected to updateCountDescription, which has a signature of

    Where did that boolean argument go? PyQt is actually smart enough to only pass as many arguments to the receiving function as it has slots for. This makes it possible for the developer to reuse the same function/method under different conditions without having to slavishly adhere to a particular signature.
  • The “remove(PyQt_PyObject)” signal transmits a pointer to a Python object, which the rmShapemethod accepts as an argument:

Finishing Up

Only one step remains: hooking up the Make Shapes button.

Let’s test it out (click for larger image):

Success!




Leave a Reply

  • (will not be published)