Python : How can two GTK widgets interact with each other? - python

What is the proper way to interact with a button without actually clicking on it?
I have a button "button", that can, upon click :
Call the method "the_method" that will print what argument (here "filename") has been passed to it
toggle its own attributes, here its icon.
And I have a treeview, whose rows must, upon double click :
Call the method "the_method" that will print what argument (here "filename") has been passed to it
toggle "button"'s attributes, here its icon.
And only the 1st part works. The "foo" function is called (via a callback for the button, directly for the treeview item) and the argument ("filename") is retrieved OK, but how to execute part 2 of the job (changing "button"'s attributes, here its icon)?
import gtk
class Lister(object):
def __init__(self):
self.hbox = gtk.HBox()
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
treeview = gtk.TreeView(liststore)
self.hbox.pack_start(treeview, False)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
treeview.connect('row-activated', self.open_file)
treeview.append_column(col)
def open_file(self, button, *args):
Buttons().the_method(self, "foo")
class Buttons(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
def __init__(self):
self.button = gtk.Button() # THIS is the button to modify
self.hbox = gtk.HBox()
self.hbox.pack_start(self.button, False)
self.button.set_image(self.OPEN_IMAGE)
self.button.connect('clicked', self.the_method, "plop")
self.toggled = True
def the_method(self, button, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
class GUI(object):
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", self.delete_event)
vbox = gtk.VBox()
vbox.pack_start(Buttons().hbox, False, False, 1)
vbox.pack_start(Lister().hbox)
self.window.add(vbox)
self.window.show_all()
return
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()

I strongly disagree with user1146332 answer. This is not a GTK+ issue, nor a strong design issue, just an object oriented programming issue. The cause of your bug is that you call the_method like this:
Buttons().the_method(self, "foo")
This can't work, because you're mixing up two different fundamental things: a class, and an instance of a class. When you call Buttons(), you're creating a new instance of the Buttons class. Thus, as this class is not a singleton, you're in fact creating a new instance, with a new GtkButton, and end up not interacting with the button you previously created.
The solution here is to make the Lister object aware of what it needs to modify, which means storing around the Buttons instance you previously created, for example in self.button, and calling the_method on it.
self.button.the_method("foo")
Here's a slightly modified version of your code. The important thing is that the Lister instance is now aware of the Buttons instance it needs to modify.
import gtk
class Lister(object):
def __init__(self, button):
self.hbox = gtk.HBox()
self.button = button
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
treeview = gtk.TreeView(liststore)
self.hbox.pack_start(treeview, False)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
treeview.connect('row-activated', self.open_file)
treeview.append_column(col)
def open_file(self, button, *args):
self.button.the_method("foo")
class Buttons(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
def __init__(self):
self.button = gtk.Button() # THIS is the button to modify
self.hbox = gtk.HBox()
self.hbox.pack_start(self.button, False)
self.button.set_image(self.OPEN_IMAGE)
self.button.connect('clicked', self.the_method, "plop")
self.toggled = True
def the_method(self, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
class GUI(object):
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", self.delete_event)
vbox = gtk.VBox()
buttons = Buttons()
vbox.pack_start(buttons.hbox, False, False, 1)
vbox.pack_start(Lister(buttons).hbox)
self.window.add(vbox)
self.window.show_all()
return
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()
However, there's still lots of room for improvement. I suggest you don't use the __init__ function to create your widgets, but a create method that will return the toplevel widget of your widget tree. This is because you can't return anything in __init__, so it's easier to use a different method instead of raising exceptions there.
b = Buttons()
vbox.pack_start(b.create(), False, False, 1)
l = Lister(b)
vbox.pack_start(l.create(), False, False, 1)
Other improvement might be (sorry, i'm using the C naming here for GTK classes/functions, which I know better than the python one):
using a GtkToggleButton instead of tracking the button state yourself
using gtk_button_set_use_stock to tell the button to interpret the label you will set in the button as the stock id for the button (this may print the associated text too, not sure about this)
switching to GTK 3 (uses pyGObject), as this is GTK 2 code (uses pyGTK), unless you want Windows compatibility
See you on linuxfr :-)

First of all i don't know anything about python but i have some experiences with gtk+ and i'm more or less familiar with its concepts.
The first thing i noticed is that you define a class called GUI and two separate classes called Buttons and Lister. For me such approach makes only sense if you design the two last mentioned classes in a way that they are a kind of (composite) widget theirselves. So that you can instantiate them at a higher level for example in the GUI class. This would be a generic approach and makes perfectly sense if you want to reuse these new widgets.
The way you did it doesn't make sense to me. From what i have gathered so far the actual aim of Buttons and Lister is to populate your main application window with widgets, to connect callbacks to signals of those widgets and to define these callbacks as methods.
I think you limit the flexibility of gtk if you make it this way. For example you connect signals to callbacks at a point where in principle you aren't able to access all the widgets of your interface. In contrast, I prefer a common place in the code at which i connect signals to callbacks and at which i can principally pass all widgets of interest to a specific callback.
In fact one often have to act upon several widgets from inside a callback. So you have to consider to implement the callbacks as methods of your GUI class where they can principally access all elements of your user interface.
Also you should consider to design your interface with glade. That way your code would be much more legible.
Supplement (after some code pushing):
import gtk
class GUI(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
toggled = True
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", gtk.main_quit)
vbox = gtk.VBox()
self.button = gtk.Button() # THIS is the button to modify
self.button.set_image(self.OPEN_IMAGE)
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
self.treeview = gtk.TreeView(liststore)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
self.treeview.append_column(col)
vbox.pack_start(self.button, False, False, 1)
vbox.pack_start(self.treeview, False, False, 1)
self.treeview.connect('row-activated', self.the_method_wrapper, "plop")
self.button.connect('clicked', self.the_method, "plop")
self.window.add(vbox)
self.window.show_all()
return
def the_method_wrapper(self, button, *args):
self.the_method(self, "foo")
def the_method(self, button, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()

Related

PyGObject page switching with buttons

I want to switch pages with the help of buttons in Gtk.Stack. There are 3 pages, and the title bar of the application has one forward and one back button. I want it to go to the next page when the forward button is pressed, and to go to the previous page when the back button is pressed. Its current state can only switch between page 1 and page 2.
import gi, os
gi.require_version("Gtk", "3.0")
gi.require_version("Handy", "1")
from gi.repository import Gtk, Handy
Handy.init()
class MyWindow(Handy.Window):
def __init__(self):
super().__init__(title="Hello World")
self.set_default_size(500, 300)
# WindowHandle
self.handle = Handy.WindowHandle()
self.add(self.handle)
# Box
self.winbox = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL)
self.handle.add(self.winbox)
# Headerbar
self.hb = Handy.HeaderBar()
self.hb.set_show_close_button(True)
self.hb.props.title = "Stack Example"
self.winbox.pack_start(self.hb, False, True, 0)
# Stack
self.stack = Gtk.Stack()
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
self.winbox.pack_start(self.stack, True, True, 0)
# Labels
self.label = Gtk.Label(label="Page 1")
self.stack.add_titled(self.label, "page0", "Label")
self.label = Gtk.Label(label="Page 2")
self.stack.add_titled(self.label, "page1", "Label")
self.label = Gtk.Label(label="Page 3")
self.stack.add_titled(self.label, "page2", "Label")
# Headerbar button 1
self.button = Gtk.Button()
self.button = Gtk.Button.new_from_icon_name("pan-start-symbolic", Gtk.IconSize.MENU)
self.hb.pack_start(self.button)
self.button.connect('clicked', self.on_button1_clicked)
# Headerbar button 2
self.button2 = Gtk.Button()
self.button2 = Gtk.Button.new_from_icon_name("pan-end-symbolic", Gtk.IconSize.MENU)
self.hb.pack_start(self.button2)
self.button2.connect("clicked", self.on_button2_clicked)
def on_button1_clicked(self, widget):
self.stack.set_visible_child_name("page1")
def on_button2_clicked(self, widget):
self.stack.set_visible_child_name("page2")
win = MyWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
I don't know if there's an easy way to make visible the next child in a GtkStack or if another container has this functionality apart from GtkAssistant.
Nevertheless there's multiple ways you can implement this yourself. Either like so:
def on_button1_clicked(self, widget):
pages = self.stack.get_children()
cur_page = self.stack.get_visible_child()
i = pages.index(cur_page)
if i == 0: return
self.stack.set_visible_child(pages[i-1])
def on_button2_clicked(self, widget):
pages = self.stack.get_children()
cur_page = self.stack.get_visible_child()
i = pages.index(cur_page)
if i == len(pages) - 1: return
self.stack.set_visible_child(pages[i+1])
where you get the stack's children with GtkContainer.get_children(), find the index of the current visible child and then plus/minus one to get the next/prev page.
Caveat: I'm not sure if get_children() always returns the child widgets in the order they are added.
Alternatively in your __init__() function, you can create a list to store your page names/widgets e.g. self.page_names = ['page0', 'page1', 'page2']. And then you can do:
def on_button1_clicked(self, widget):
cur_page_name = self.stack.get_visible_child_name()
i = self.page_names.index(cur_page_name)
if i == 0: return
self.stack.set_visible_child_name(self.page_names[i-1])
Or maybe you extract the page number from the child name (e.g. 0 from page0) using RegEx and generate the next page's name. There's many ways to accomplish this, I personally would keep a variable of all the pages and use that to determine which page is next/prev.

QWidgetAction checked state not changing when using setDefaultWidget to a QCheckBox

I want to create a menu with a checkable list. To prevent the menu from closing when the action is clicked, I'm setting the DefaultWidget to be a QCheckBox. The problem is when I'm trying to get isClicked from the action - it doesn't seem to be synced to the checkbox. How do I get the value of the action to change when the checkbox is clicked?
tool_button = QtWidgets.QToolButton()
menu = QtWidgets.QMenu()
check_box = QtWidgets.QCheckBox(menu)
check_box.setText("abc")
check_box.setChecked(True)
action_button = QtWidgets.QWidgetAction(menu)
action_button.setDefaultWidget(check_box)
menu.addAction(action_button)
tool_button.setMenu(menu)
print(check_box.text()) # returns abc
print(check_box.isChecked()) # returns True
print(action_button.isChecked()) # returns False - it's not picking up the values from check_box
Since QWidgetAction acts as some sort of container for any kind of widget, it has no way to know if its defaultWidget could even have any support for a bool state like "isChecked", so you have to provide it.
The simplest way is to subclass a specific QWidgetAction class for that action, and override its isChecked() method so that it returns the checked value based on its default widget.
from PyQt5 import QtWidgets
class CheckableWidgetAction(QtWidgets.QWidgetAction):
def setDefaultWidget(self, widget):
super().setDefaultWidget(widget)
try:
# if the widget has the toggled signal, connect that signal to the
# triggered signal
widget.toggled.connect(self.triggered)
except:
pass
def isChecked(self):
try:
return self.defaultWidget().isChecked()
except:
# failsafe, in case no default widget has been set or the provided
# widget doesn't have a "checked" property
return super().isChecked()
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
tool_button = QtWidgets.QToolButton()
layout.addWidget(tool_button)
menu = QtWidgets.QMenu()
check_box = QtWidgets.QCheckBox(menu)
check_box.setText("abc")
check_box.setChecked(True)
self.action_button = CheckableWidgetAction(menu)
self.action_button.setDefaultWidget(check_box)
self.action_button.triggered.connect(self.action_triggered)
menu.addAction(self.action_button)
tool_button.setMenu(menu)
controlButton = QtWidgets.QPushButton('is checked?')
layout.addWidget(controlButton)
controlButton.clicked.connect(self.is_checked)
def is_checked(self):
print('checked is {}'.format(self.action_button.isChecked()))
def action_triggered(self, state):
print('triggered {}'.format(state))
if __name__ == '__main__':
import sys
a = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(a.exec_())

Homogeneous children in ButtonBox

I added a few children to a ButtonBox, and I wanted them not the be homogeneous. So I called ButtonBox.set_homogeneous(False) and mostly it worked. But when I resize the window bellow the minimum size, and a vertical scrollbar appears, I see there is a lot of empty space bellow the ButtonBox. I was able to fix this by individually specifying each children as non homogeneous calling ButtonBox.set_child_non_homogeneous(child, True), while also leaving in the previous call to ButtonBox.set_homogeneous(False).
My question is then, why does this happen? I set the ButtonBox's layout to EXPAND, all the space should be taken.
I made a little test code to illustrate what I'm talking about. You can try it with and without the commented line to see both cases I mentioned.
import sys
from gi.repository import Gtk
class Application(Gtk.Application):
def __init__(self):
super().__init__(application_id='com.stackoverflow.xor')
self.connect('activate', self.on_activate)
self.connect('startup', self.on_startup)
def on_startup(self, app):
self.window = Gtk.ApplicationWindow(application=app)
self.window.set_default_size(200, 200)
self.window.add(MainView(self))
def on_activate(self, app):
self.window.show_all()
class MainView(Gtk.ScrolledWindow):
def __init__(self, app):
super().__init__()
button_list = Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL)
button_list.set_layout(Gtk.ButtonBoxStyle.EXPAND)
button_list.set_homogeneous(False)
button_list.get_style_context().remove_class('linked')
for i in range(4):
button = Gtk.Button()
label = Gtk.Label('\n'.join(['test test'] * (i + 1)))
button.add(label)
button_list.pack_start(button, False, False, 0)
#button_list.set_child_non_homogeneous(button, True)
self.add(button_list)
if __name__ == '__main__':
main_app = Application()
exit_status = main_app.run(sys.argv)
sys.exit(exit_status)
Your issue is when you are adding the buttons to the box you set Expand to False.
button_list.pack_start(button, False, False, 0)
The buttonbox having an EXPAND layout is not the same as its children expanding themselves.
EDIT: This was discussed on #gtk+ but the children should all have Expand and Fill set to True for this layout.

Need help in connecting Connections in a class

I have created a window with QTableWidget having a cell with 2 buttons.
Buttons are created in seperate class where i am passing QTableWidget instance from main procedure.
I am not able to get the button events, which are connected in button Creation class. My code snippet is as below
class Buttons():
def __init__(self,tab):
buttonLayout = QtGui.QHBoxLayout()
buttonLayout.setContentsMargins(0,0,0,0)
self.saveButtonItem = QtGui.QPushButton('Save')
self.deleteButtonItem = QtGui.QPushButton('Delete')
buttonLayout.addWidget(self.saveButtonItem)
buttonLayout.addWidget(self.deleteButtonItem)
cellWidget = QtGui.QWidget()
cellWidget.setLayout(buttonLayout)
tab.insertRow(tab.rowCount())
tab.setCellWidget(tab.rowCount() - 1,0,cellWidget)
self.setconncection()
def setconncection(self):
self.saveButtonItem.clicked.connect(self.btnSaveClicked)
self.deleteButtonItem.clicked.connect(self.btnDeleteClicked)
print 'connections are set'
def btnSaveClicked(self):
print 'save clicked'
def btnDeleteClicked(self):
print 'delete clicked'
class testing(QtGui.QTableWidget):
def __init__(self):
super(testing,self).__init__()
self.setColumnCount(1)
for i in xrange(3):
self.r = Buttons(self)
if __name__ == "__main__" :
import sys
app = QtGui.QApplication (sys.argv)
win = testing ()
win.show()
sys.exit(app.exec_())
My window at run time is as below
After the __init__ of testing, the reference to Buttons instance is lost and the object is destroyed. (Variable r is affected but not used.)
Keeping a link to it (see last line in following code snippet) makes it work.
class testing(QtGui.QTableWidget):
def __init__(self):
super(testing,self).__init__()
self.setColumnCount(1)
self.setRowCount(1)
self.buttons = []
for i in xrange(3):
self.buttons.append(Buttons(self))

Python making gtk.Layout with Scrollbars

How could I have a scrollbar inside a gtk.Layout.
For example, in my code I have:
import pygtk
pygtk.require('2.0')
import gtk
class ScrolledWindowExample:
def __init__(self):
self.window = gtk.Dialog()
self.window.connect("destroy", self.destroy)
self.window.set_size_request(300, 300)
self.scrolled_window = gtk.ScrolledWindow()
self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.window.vbox.pack_start(self.scrolled_window, True, True, 0)
self.layout = gtk.Layout()
self.scrolled_window.add(self.layout)
self.current_pos = 0
self.add_buttom()
self.window.show_all()
def add_buttom(self, widget = None):
title = str(self.current_pos)
button = gtk.ToggleButton(title)
button.connect_object("clicked", self.add_buttom, None)
self.layout.put(button, self.current_pos, self.current_pos)
button.show()
self.current_pos += 20
def destroy(self, widget):
gtk.main_quit()
if __name__ == "__main__":
ScrolledWindowExample()
gtk.main()
What I really want is to find some way to make the scroll dynamic. See the example that I put above, when you click any button, another button will be added. But the scrollbar doesn't work.
What can I do to get the scroll bars working?
Does it works if you either use gtk.Window() instead of gtk.Dialog(); or execute self.window.run() after self.window.show_all()?
The difference between Dialog and common Window is that Dialog has its own loop which processes events. As you do not run its run() command, this loop never gets the chance to catch the events, so ScrolledWindow does not receives them, and does not change its size.

Categories

Resources