PyQt connect inside for loop vs. separate calls results in different behavior

949 views Asked by At

I'm building a plotting UI that lets a user generate multiple plots from loaded data sets. As part of this the user can add marker lines to their plots to examine (x, y) values by moving those marker lines across the plot.

The functions below work fine if the markers are added to each plot separately (i.e. add_marker to Plot1, Plot2, etc. separately via the elif block of code). However, if the "add to all" option is selected, resulting in the usage of the for loop block of code in the add_marker function all of the markers end up being children of the last plotItem in the list (plot_objects).

If I check the marker objects as the loop iterates when the add_marker function is called the objects, and their parents, are distinct. However, if I check the parents in the update_marker_vals function the parent for markers 1 and 2 are incorrect for all but the last plot in the list.

Not sure what's going on here, but I assume it has something to do with the two sigDragged.connect statements, seeing as everything else seems fine before then.

Code:

def add_marker(self):
        name = self.markersPlot_dropdown.currentText()

        if name == "All":
            for plot_name, plt in self.plot_objects.items():
                x_val = (plt.viewRange()[0][0]+plt.viewRange()[0][1])/2
                marker_one = plt.addLine(x=x_val*0.5, pen=pg.mkPen('g', width=2.0), movable=True)
                marker_two = plt.addLine(x=x_val*1.5, pen=pg.mkPen('c', width=2.0), movable=True)
                marker_one.sigDragged.connect(lambda: self.update_marker_vals(marker_one, "Marker One"))
                marker_two.sigDragged.connect(lambda: self.update_marker_vals(marker_two, "Marker Two"))

                self.plot_markers[plot_name] = {"Marker One": marker_one, "Marker Two:": marker_two}

        elif name:
            plt = self.plot_objects[name]
            x_val = (plt.viewRange()[0][0]+plt.viewRange()[0][1])/2
            marker_one = plt.addLine(x=x_val*0.5, pen=pg.mkPen('g', width=2.0), movable=True)
            marker_two = plt.addLine(x=x_val*1.5, pen=pg.mkPen('c', width=2.0), movable=True)
            marker_one.sigDragged.connect(lambda: self.update_marker_vals(marker_one, "Marker One"))
            marker_two.sigDragged.connect(lambda: self.update_marker_vals(marker_two, "Marker Two"))

            self.plot_markers[name] = {"Marker One": marker_one, "Marker Two:": marker_two}

    def update_marker_vals(self, marker, marker_num):
        plot_item = marker.parentItem().parentItem().parentItem()
        plot_name = list(self.plot_objects.keys())[list(self.plot_objects.values()).index(plot_item)]
        sampling_divisor = self.selected_curve[plot_name].getData()[0][1] - \
                           self.selected_curve[plot_name].getData()[0][0]
        index = int(marker.getXPos() / sampling_divisor)
        x_val = self.selected_curve[plot_name].getData()[0][index]
        y_val = self.selected_curve[plot_name].getData()[1][index]
        if marker_num == "Marker One":
            print(plot_name)
            print("Marker One, :" + str(index))
            print(x_val, y_val)

        elif marker_num == "Marker Two":
            print(plot_name)
            print("Marker Two, :" + str(index))
            print(x_val, y_val) 

Edit:

On a side note, as a solution I can separate this function out into two functions - one function that creates the two markers and then another function that takes the input from the QComboBox and creates one set of markers for a specific plot or creates markers for all the plots available. This works, and is my current solution, but I'm still curious as to what the issue is with the above code.

1

There are 1 answers

0
three_pineapples On BEST ANSWER

When you connect a signal to a lambda function, the contents of the lambda function are evaluated when the signal is emitted, not when the signal is connected. As such, the variables you use (marker_one and marker_two) always point to the objects created in the last iteration of the loop.

One simple solution is to explicitly pass in marker_one and marker_two as default arguments to variables of the same name, in the signature of the lambda function:

lambda marker_one=marker_one: self.update_marker_vals(marker_one, "Marker One")
lambda marker_two=marker_two: self.update_marker_vals(marker_two, "Marker Two")

There are several useful answers relating to a very similar problem here, specifically the answer by ekhumoro, if you would like to know more (my answer to that question my also be of use, although ekhumoro's solution is cleaner)