Question about using QT to generate an additional UI

edited April 2015 in Ruby Scripting

Hi Matthias,

I tried to use QT to generate a window. Below is my code. I created a icon on top tool bar, then the window can be created after pressing the icon. But the problem i met is that as i drag the winodw edge trying to adjust its size. When i keep doing this action around few seconds, the Klayout is crashed. Could you help clarify the cause ?? Many thanks.

Best regards,
Covalent

class MenuHandler < RBA::Action
def initialize( t, &action ) 
    self.title = t
    @action = action    
end
def triggered
    @action.call( self ) 
end
private
@action
end

class UI_Main
include RBA

def gen_ui
    wdg = QWidget.new
    wdg.setWindowTitle("Windows")

    wdg.resize 350, 300
    wdg.move 300, 300

    wdg.show

    vbox = QVBoxLayout.new
    vbox1 = QVBoxLayout.new
    hbox1 = QHBoxLayout.new
    hbox2 = QHBoxLayout.new

    windowlabel1 = QLabel.new("Windows")
    edit = QTextEdit.new
    edit.enabled = false

    activate = QPushButton.new("Activate")
    close = QPushButton.new("Close")
    help = QPushButton.new("Help")
    ok = QPushButton.new("OK")

    vbox.addWidget(windowlabel1)

    vbox1.addWidget(activate)
    vbox1.addWidget(close, 0, 0x0020)
    hbox1.addWidget(edit)
    hbox1.addLayout(vbox1)

    vbox.addLayout(hbox1)

    hbox2.addWidget(help)
    hbox2.addStretch
    hbox2.addWidget(ok)

    vbox.addLayout(hbox2, 1)
    wdg.setLayout(vbox)

end
end

class UI_Test   
def initialize
    UI_Main.new.gen_ui
end
end

# ---------------------------------------------------------------------------
#  Main application
app = RBA::Application.instance
mw = app.main_window
@menu_handler = MenuHandler.new("RBA test"){UI_Test.new}
mw.menu.insert_item("@toolbar.end", "rba_test", @menu_handler)  

app.exec

Comments

  • edited November -1

    Hi covalent,

    I have used the Qt binding to do similar things. Looking at your code, there are two issues that come to my mind:

    1. You don't need to call app.exec at the end. This seems to be a pattern in creating standalone Qt applications but in our case, the application (i.e. KLayout) is already running.

    2. Although I don't think I ever saw it crashing, KLayout version 0.23 does have some focus issues with the Macro Development window when it is open. If you want to know more about it, I'd recommend reading about it here, the bottom line is that this issue seems to be fixed with KLayout version 0.24-python-eval which I can highly recommend. Keep in mind that you'll have to change some Qt constants in your code to make it work.

    I was able to run your code (without the app.exec line) in version 0.23, but I had to close the Macro Development window in order to be able to resize your window.

    If this information doesn't solve your issue, could you also tell me which KLayout version you're running and on which platform (Windows, Linux...)?

  • edited April 2015

    Hi friendfx,

    Thanks for your reply. I use 0.23.4 on win7.
    The reason i add app.exec in the end is that i don't use Macro Developement window to run this code. I use the way of klayout.exe -r $my_ruby_code_path to run it, so i have to use app.exec to call klayout main window.
    I don't know if my problem is similar to yours. But if i changed my ui_content as simple as below. There won't be the problem i met before.

    def gen_ui
      wdg = QWidget.new
      wdg.setWindowTitle("Windows")
    
      wdg.resize 350, 300
      wdg.move 300, 300
    
      wdg.show
    
      vbox = QVBoxLayout.new
    
      windowlabel1 = QLabel.new("Windows")
      vbox.addWidget(windowlabel1)
      wdg.setLayout(vbox)
    
    end
    
  • edited April 2015

    Hi covalent,

    I am currently running version 0.23.10 under Win7. I just tried your code (including the app.exec line at the end) via klayout -r Discussion611.lym.

    Discussion611.lym is your complete code (copy-pasted into the Macro editor) from the first post, wrapped between module Discussion611 and end lines.

    This works perfectly for me, the button shows up and when I click it, the window opens and is resizable as expected.

    UPDATE

    ...at least so I thought. I guess I wasn't resizing the window for long enough :-)

    Strange. In the Task Manager, I can see that the memory consumption by klayout.exe increases while I am resizing the window and it doesn't go back down once I release the mouse button. I even tried several shorter resize durations, each one increasing the memory consumption.

    And at some point, the GUI of the application freezes and I have to force-close it.

    I also tried this with version 0.24-python-eval (had to replace your constant with Qt::AlignTop), same issue.

    UPDATE 2

    Trying this on version r2739 on my Ubuntu VM actually crashed the program once I clicked the button, leading to a stack trace and the following message:

    [NOTE]
    You may have encountered a bug in the Ruby interpreter or extension libraries.
    Bug reports are welcome.
    For details: http://www.ruby-lang.org/bugreport.html
    
    
    [1]+  Aborted                 klayout/klayout-r2739/build.linux-64-gcc-release/main/klayout
    

    I also tried with the official Ubuntu 0.23.10 version, which let me click the button fine but the resize problem is the same as under windows - eventually causing a stacktrace ending with the same message as shown above. For reference, on my Ubuntu VM, KLayout seems to use Ruby version 1.9.3-p484 (x86_64-linux).

    If I was in your shoes, the next thing I'd try would be

    1. to further narrow down which specific QWidget causes this problem and
    2. to test if it also occurs in Python (of course, in the 0.24-python-eval version).
  • edited November -1

    Hi friendfx,

    Thanks for your test. I keep trying the find the cause. Btw, I also see the memory consumption problem.

    Regareds,
    covalent

  • edited April 2015

    Hi covalent,

    I think the problem is caused by a lifetime issue: if you create a Widget in a Ruby variable, Ruby basically controls the lifetime of the widget. But the variable "wdg" goes out of scope, so the Ruby garbage collector is free to destroy the object when it wants to.

    I think what happens now is that after leaving the function, the widget is shown because the Widget object is not destroyed yet. But after some time, the garbage collector will release the memory and your object will be lost causing the crash.

    The easiest solution is to give the widget a parent, for example the main window:

    wdg = QWidget.new(Application.instance.main_window)
    

    When you do so, the ownership will be transferred to Qt and Ruby does not delete the Widget. As a general rule, every widget should have a parent, except very top level windows. For those you could use global variables to keep the objects. These are not cleaned up, because they don't go out of scope.

    But if you use a parent for the widget, you should use "WA_DeleteOnClose" because your object will persist if you close the window causing a memory leak.

    Matthias

  • edited April 2015

    Hi Matthias,

    Thanks for your explanation. After I add a parent to the variable "wdg", the "wdg" window is embedded into klayout window and can't be moved or adjusted for window size. I want it floating. Please see below modified code for my class of "UI_Main". As I set 0x00000001 to the window flag of "wdg" as "wdg = QWidget.new(app, 0x00000001)", the "wdg" window can be floting for moving or adjusting. But when i keep resizing its window size by draging, same problem of klayout crash still happens. I think there must be something wrong in my logistic of coding. Hopefully i could know more from you. Thanks a lot.

    covalent

    class UI_Main
    include RBA
    def gen_ui
        app = Application.instance.main_window
    
        wdg = QWidget.new(app)
        wdg.setWindowTitle("Windows")
        wdg.resize 350, 300
        wdg.move 300, 300
    
        vbox = QVBoxLayout.new(wdg)
        vbox1 = QVBoxLayout.new(wdg)
        hbox1 = QHBoxLayout.new(wdg)
        hbox2 = QHBoxLayout.new(wdg)
    
        windowlabel1 = QLabel.new("Windows", wdg)
        edit = QTextEdit.new(wdg)
        edit.enabled = false
    
        activate = QPushButton.new("Activate", wdg)
        close = QPushButton.new("Close", wdg)
        help = QPushButton.new("Help", wdg)
        ok = QPushButton.new("OK", wdg)
    
        vbox.addWidget(windowlabel1)
    
        vbox1.addWidget(activate)
        vbox1.addWidget(close, 0, 0x0020)
        hbox1.addWidget(edit)       
    
        hbox2.addWidget(help)
        hbox2.addStretch
        hbox2.addWidget(ok)
    
        hbox1.addLayout(vbox1)
        vbox.addLayout(hbox1)
        vbox.addLayout(hbox2, 1)
    
        wdg.setLayout(vbox)     
        wdg.show
    
    end
    end
    
  • edited April 2015

    Hi covalent,

    I think the reason why you see it "embedded" within the main KLayout window is because wdg is a QWidget, not a QDialog or QMainWindow. I have adapted the code to the latter, as follows, for my 0.24-python-eval version:

    def gen_ui
    
        main_win = QMainWindow.new(Application.instance.main_window)
        wdg = QWidget.new(main_win)
        wdg.setWindowTitle("Windows")
    
        wdg.resize 350, 300
        wdg.move 300, 300
    
    
        vbox = QVBoxLayout.new
        vbox1 = QVBoxLayout.new
        hbox1 = QHBoxLayout.new
        hbox2 = QHBoxLayout.new
    
        windowlabel1 = QLabel.new("Windows")
        edit = QTextEdit.new
        edit.enabled = false
    
        activate = QPushButton.new("Activate")
        close = QPushButton.new("Close")
        help = QPushButton.new("Help")
        ok = QPushButton.new("OK")
    
        vbox.addWidget(windowlabel1)
    
        vbox1.addWidget(activate)
        vbox1.addWidget(close, 0, Qt::AlignTop)
        hbox1.addWidget(edit)
        hbox1.addLayout(vbox1)
    
        vbox.addLayout(hbox1)
    
        hbox2.addWidget(help)
        hbox2.addStretch
        hbox2.addWidget(ok)
    
        vbox.addLayout(hbox2, 1)
        wdg.setLayout(vbox)
    
        main_win.setCentralWidget(wdg)
        main_win.show
    
    end
    end
    

    Now that has the wdg as a child widget to a QMainWindow instance, which in turn has KLayout's main window as its parent.

    Interestingly, this again exhibits the freeze as discussed earlier, at least for me under Win7.

  • edited November -1

    Ok, I tried to completely follow Matthias' advice on properly "parenting" each and every one of the widgets in the code. I also removed the menu parts and app.exec to make it a simple macro to be run from the Macro Development window. Here's the complete code:

    module Discussion611
    
    include RBA
    
    def self.gen_ui
    
        $main_win = QMainWindow.new(Application.instance.main_window)
        wdg = QWidget.new($main_win)
        wdg.setWindowTitle("Windows")
    
        wdg.resize 350, 300
        wdg.move 300, 300
    
    
        vbox = QVBoxLayout.new(wdg)
        vbox1 = QVBoxLayout.new(wdg)
        hbox1 = QHBoxLayout.new(wdg)
        hbox2 = QHBoxLayout.new(wdg)
    
        windowlabel1 = QLabel.new("Windows", wdg)
        edit = QTextEdit.new(wdg)
        edit.enabled = false
    
        activate = QPushButton.new("Activate", wdg)
        close = QPushButton.new("Close", wdg)
        help = QPushButton.new("Help", wdg)
        ok = QPushButton.new("OK", wdg)
    
        vbox.addWidget(windowlabel1)
    
        vbox1.addWidget(activate)
        vbox1.addWidget(close, 0, Qt::AlignTop)
        hbox1.addWidget(edit)
        hbox1.addLayout(vbox1)
    
        vbox.addLayout(hbox1)
    
        hbox2.addWidget(help)
        hbox2.addStretch
        hbox2.addWidget(ok)
    
        vbox.addLayout(hbox2, 1)
        wdg.setLayout(vbox)
    
        $main_win.setCentralWidget(wdg)
        $main_win.show
    
    end
    
    puts $main_win
    app = RBA::Application.instance
    
    gen_ui
    
    #app.exec
    
    end
    

    The window (now a global constant and tied to the main KLayout window) still freezes (along with the KLayout main and Macro Develoment windows) when resized, under Windows 7.

    Under Ubuntu (version r2739), simply starting this script crashes KLayout with the You may have encountered a bug in the Ruby interpreter... note.

  • edited April 2015

    Hi all,

    I think i found the cause of klayout crash problem. As i add "nil" action to the buttons, no matter how or how long i keep resizing the window size, there won't be klayout crash problem. So the button without corresponding action would make klayout crash. please refer to my code below. But the problem of memory accumulation while resizing the window size still exist.

    covalent

    class MenuHandler < RBA::Action
    def initialize( t, &action ) 
        self.title = t
        @action = action    
    end
    def triggered
        @action.call( self ) 
    end
    private
    @action
    end
    
    class UI_Main
    
    include RBA
    
    def gen_ui
        wdg = QWidget.new
        wdg.setWindowTitle("Windows")
        wdg.resize 350, 300
        wdg.move 300, 300
    
        vbox = QVBoxLayout.new(wdg)
        vbox1 = QVBoxLayout.new(wdg)
        hbox1 = QHBoxLayout.new(wdg)
        hbox2 = QHBoxLayout.new(wdg)
    
        windowlabel1 = QLabel.new("Windows", wdg)
        edit = QTextEdit.new(wdg)
        edit.enabled = false
    
        activate = QPushButton.new("Activate", wdg)
        close = QPushButton.new("Close", wdg)
        help = QPushButton.new("Help", wdg)
        ok = QPushButton.new("OK", wdg)
    
        activate.clicked {nil}
        close.clicked {nil}
        help.clicked {nil}
        ok.clicked {nil}
    
        vbox.addWidget(windowlabel1)
    
        vbox1.addWidget(activate)
        vbox1.addWidget(close, 0, 0x0020)
        hbox1.addWidget(edit)       
    
        hbox2.addWidget(help)
        hbox2.addStretch
        hbox2.addWidget(ok)
    
        hbox1.addLayout(vbox1)
        vbox.addLayout(hbox1)
        vbox.addLayout(hbox2, 1)
    
        wdg.setLayout(vbox)     
        wdg.show
    
    end
    end
    
    class UI_Test   
    def initialize
        UI_Main.new.gen_ui
    end
    end
    
    # ---------------------------------------------------------------------------
    #  Main application
    app = RBA::Application.instance
    mw = app.main_window
    @menu_handler = MenuHandler.new("RBA test"){UI_Test.new}
    mw.menu.insert_item("@toolbar.end", "rba_test", @menu_handler)  
    
    app.exec
    
  • edited April 2015

    This may or may not be a helpful comment. My apologies if it is not...

    Did you know you can create complicated ui's using QT Creator, save it as a .ui file, and then call it in KLayout script? It's possible you weren't aware of that solution (but it's also possible that you want to code all this QT stuff inside KLayout for your own reasons).

    Because then the Qt stuff is not owned by ruby and so ruby's garbage collection "can't touch this" -- I believe.

    Anyway, that could solve the problem -- even if only in the short term. Or, it could be actually a better solution for you (less coding, but more mouse clicking..).

    Here's an example.

  • edited April 2015

    Hi all,

    I can reproduce the issue, although not that easily. Plus I can give an explanation:

    The problem is basically the nesting of layouts. As mentioned earlier, it's not possible for Ruby to tell whether an object is owned by Qt without further knowledge. If the object has a parent, it will assume that Qt has the ownership. That works for the widgets, but not for the layouts. Except for the first layout, Qt refuses to use the widget as the parent (that's why you get messages like "trying to add a layout to a widget that already has one"). For the three following layouts there is no parent and Qt will assume ownership. Only later on, these layouts are added as sub-layouts, but at this point, Ruby has already taken over the ownership and will delete these layouts when the garbage collector cleans up memory. That will cause the crash.

    Bottom line is, nested layouts don't work reliably when you create them explicitly. I'll try to fix this in later versions.

    But David is right because using .ui files creates the layouts without interaction of Ruby and the program does not crash.

    Another workaround is to not use nested layouts. You can use frames as sub-widgets.
    Or you can use a single grid layout in your case:

    module Discussion611
    
    include RBA
    
    def self.gen_ui
    
        $wdg && !$wdg.destroyed? && $wdg.destroy
    
        wdg = QDialog.new(Application.instance.main_window)
        $wdg = wdg
    
        wdg.setAttribute(Qt::WA_DeleteOnClose)
        wdg.setWindowTitle("Windows")
    
        wdg.resize 350, 300
        wdg.move 300, 300
    
        grid = QGridLayout.new(wdg)
    
        windowlabel1 = QLabel.new(wdg)
        windowlabel1.setText("Windows")
        edit = QTextEdit.new(wdg)
        edit.enabled = false
    
        activate = QPushButton.new("Activate", wdg)
        close = QPushButton.new("Close", wdg)
        help = QPushButton.new("Help", wdg)
        ok = QPushButton.new("OK", wdg)
    
        grid.addWidget(windowlabel1, 0, 0, 1, 3)
        grid.addWidget(edit, 1, 0, 3, 2)
        grid.addWidget(activate, 1, 3)
        grid.addWidget(close, 2, 3)
        grid.addWidget(help, 4, 0)
        grid.addWidget(ok, 4, 3)
        grid.setRowStretch(3, 1)
        grid.setColumnStretch(1, 1)
    
        wdg.show
    
    end
    
    gen_ui
    
    end
    

    Please also note that this code will avoid keeping "stale" window instances by deleting existing ones if one exists. I also added "WA_DeleteOnClose" to delete the object when the window is closed.

    This version does no longer crash on my installation.

    Matthias

  • edited April 2015
    Hi Matthias,

    I used your above code to insert widgets in grid layout. As you said, no klayout crash problem. Thanks for your cause identification.
    But there is still another problem that the memory usage of klayout will keep accumulating as i opened the wdg window but did nothing. I did a test that as i opened the klayout and call the wdg window, the memory showed on my windows task manager is about 82000K. After 10 minutes, the memory usage became about 100000K. In this 10 minute period, i didn't do anything. Then when i close the wdg window, the memory wasn't released to orignal 82000K. Btw if you resize the window, the memory accumulation speed seems more obvious. Could i ask if you also have such problem or it is my own problem ?

    Best regards,
    covalent
  • edited November -1

    Hi covalent,

    I can see some memory allocation of roughly the same magnitude, but it seems to saturate in my case after allocating 20M of memory. I'll have to investigate that, I think it's the cost of some Ruby objects being allocated in background event handlers. To me it looks like the garbage collector is not quite effective then.

    Do you observe saturation too? Does the memory still increase after you explicitly call the GC (run "GC.start") in the macro IDE's console)?

    I shall try that on Python too - Python has a different memory management model based on reference counting and should show a different behaviour.

    Matthias

  • edited April 2015

    Hi all,

    here is my translation to Python:

    import pya
    
    def gen_ui():
      global wdg
      if 'wdg' in globals():
        if wdg is not None and not wdg.destroyed():
          wdg.destroy()
    
      wdg = pya.QDialog(pya.Application.instance().main_window())
    
      wdg.setAttribute(pya.Qt.WA_DeleteOnClose)
      wdg.setWindowTitle("Windows")
    
      wdg.resize(350, 300)
      wdg.move(300, 300)
    
      grid = pya.QGridLayout(wdg)
    
      windowlabel1 = pya.QLabel(wdg)
      windowlabel1.setText("Windows")
      edit = pya.QTextEdit(wdg)
      edit.enabled = False
    
      activate = pya.QPushButton("Activate", wdg)
      close = pya.QPushButton("Close", wdg)
      help = pya.QPushButton("Help", wdg)
      ok = pya.QPushButton("OK", wdg)
    
      grid.addWidget(windowlabel1, 0, 0, 1, 3)
      grid.addWidget(edit, 1, 0, 3, 2)
      grid.addWidget(activate, 1, 3)
      grid.addWidget(close, 2, 3)
      grid.addWidget(help, 4, 0)
      grid.addWidget(ok, 4, 3)
      grid.setRowStretch(3, 1)
      grid.setColumnStretch(1, 1)
    
      wdg.show()
    
    gen_ui()
    
    print(wdg)
    

    Using this code, I still see increasing memory consumption on the Windows 7 0.24-python-eval version. But I found something else that might help to pinpoint the problem:

    • The memory consumption only increases if a button has the focus. On Windows, this is shown as the button having a blue frame around it and at the same time, the colour fades from grey to faint blue and back.
    • If a button has the focus, I can see that the klayout process constantly burns CPU cycles. Not too many (4% on my machine), but still more than I would expect from an application sitting idle.
    • There seems to be a slight difference between the "Activate" button and the others: When one of the other buttons has the focus and the focus of the window is lost, the button that used to have the focus also loses it, i.e. it doesn't do the fade and the blue frame is gone. No CPU cycles are used, no memory increase happens. If the "Activate" button has the focus, sometimes switching to a different window does not make the "Activate" button lose the focus - it is still fading grey to blue and back, CPU cycles are used and memory consumption increases.

    FriendFX

  • edited November -1
    Hi all,

    I did same test which friendfx did. The memory indeed keep inceasing while button got focus. When i click klayout main window to make the wdg window including the buttons lose focus, the memory stop increase. Regarding memory increase saturation, i didn't see it. i tested to make wdg window with "Activate" button with focus idle more than 30min, the klayout memory increase from 86M to 120M.
    In addition to the problem of memory increase due to the "button focus" issue, to resize the window size also make memory usage increase quickly as observed by friendfx previously. i did a test that to keep dragging the window edge adjusting its window size, the memory usage increases to more than 200M, after release the mouse button, the memory still wasn't released.

    covalent
  • edited November -1

    Hi all,

    thanks for performing these tests.

    I see some memory increase but not as pronounced. I assume the problem is caused by some events being sent to the widgets continuously (timer, mouse move etc.). A Qt widget needs the ability to reimplement event handler methods and this will cause a little allocation of memory in the script's interpreter space.

    I doubt that that's a real leak but apparently the memory is not properly made available again. I'll try to debug this. I would help to know whether calling GC.start will at least slow down the memory consumption. I my case (Ubuntu) it seems to do.

    Matthias

Sign In or Register to comment.