I am trying to create this kind of list-like user interface gui (without Glade) within my InputPage object, that can be seen throughout gnome3 user interface:
and all I have is this (note that I would only like to add the buttons + and - with the same style they appear in the first image, I would like to keep columns of my list just as it is):
for some reason buttons are wider than expected and the toolbar doesnt fill the horizontal space.
here is my code:
class InputPage(Gtk.Box):
def __init__(self):
Gtk.Box.__init__(self)
self.grid = Gtk.Grid()
self.grid.set_column_homogeneous(True)
self.grid.set_row_homogeneous(True)
self.add(self.grid)
#Creating the ListStore model
self.software_liststore = Gtk.ListStore(str, str, int)
for software_ref in software_list:
self.software_liststore.append(list(software_ref))
#creating the treeview, making it use the filter as a model, and adding the columns
self.treeview = Gtk.TreeView()
for i, column_title in enumerate(["Name", "Frequency", "Amplitude"]):
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
self.treeview.append_column(column)
#button.connect("clicked", self.on_selection_button_clicked)
#setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row
self.scrollable_treelist = Gtk.ScrolledWindow()
self.scrollable_treelist.set_vexpand(True)
self.grid.attach(self.scrollable_treelist, 0, 0, 10, 7)
#Toolbar
list_add = Gtk.Button()
list_add.add(Gtk.Image(icon_name='list-add-symbolic', visible=True))
list_insert_object = Gtk.Button()
list_insert_object.add(Gtk.Image(icon_name='insert-object-symbolic', visible=True))
list_remove = Gtk.Button()
list_remove.add(Gtk.Image(icon_name='list-remove-symbolic', visible=True))
self.toolbar = Gtk.ButtonBox(spacing=5)
self.toolbar.get_style_context().add_class('inline-toolbar')
self.toolbar.add(list_add)
self.toolbar.add(list_remove)
self.toolbar.add(list_insert_object)
self.toolbar.set_hexpand(True)
self.grid.attach(self.toolbar, 0,7,4,1)
#self.grid.attach_next_to(self.toolbar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1)
# for i, button in enumerate(self.buttons[1:]):
#self.grid.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
self.scrollable_treelist.add(self.treeview)
self.show_all()
What you see on the bottom of the first screenshot is a standard GtkToolbar with the inline-toolbar style class.
A GtkToolbar does not necessarily need to be on the top of a window. You can place them anywhere, just like regular widgets. All widgets have a style context that defines how the widget looks; each style context has one or more classes applied to it. These classes are CSS classes; styling a widget is just like saying class="classname" in HTML.
Here is one possible way to do this:
Create a vertical GtkBox with no spacing.
Place the GtkTreeView as the first child of the GtkBox.
Create a GtkToolbar and place it as the second child of the GtkBox.
Call the get_style_context() method on the GtkToolbar.
Call the add_class() method of the style context, passing the string "inline-toolbar". (I believe there is a symbolic constant for this; I do not know what this is in Python.)
Recreate your buttons as GtkToolButtons (NOT regular GtkButtons!) that are children of the GtkToolbar.
If that doesn't result in the buttons looking connected, you can add the "linked" class to the toolbar in the same way.
Good luck!
Related
I have a situation where i want to add 3 buttons in a QTableWidget.
I could able to add a single button using below code.
self.tableWidget = QtGui.QTableWidget()
saveButtonItem = QtGui.QPushButton('Save')
self.tableWidget.setCellWidget(0,4,saveButtonItem)
But i want to know how to add multiple (lets say 3) buttons. I Mean Along with Save Button i want to add other 2 buttons like Edit, Delete in the same column (Actions)
You can simply create your own widget, containing the three buttons, e.g. via subclassing QWidget:
class EditButtonsWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtGui.QHBoxLayout()
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
# add your buttons
layout.addWidget(QtGui.QPushButton('Save'))
layout.addWidget(QtGui.QPushButton('Edit'))
layout.addWidget(QtGui.QPushButton('Delete'))
self.setLayout(layout)
And then, set this widget as the cellwidget:
self.tableWidget.setCellWidget(0,4, EditButtonsWidget())
You use a layout widget to add your widgets to, then add the layout widget to the cell.
There are a couple of different ones you can use.
http://doc.qt.io/qt-4.8/layout.html
self.tableWidget = QtGui.QTableWidget()
layout = QtGui.QHBoxLayout()
saveButtonItem = QtGui.QPushButton('Save')
editButtonItem = QtGui.QPushButton('Edit')
layout.addWidget(saveButtonItem)
layout.addWidget(editButtonItem)
cellWidget = QtGui.QWidget()
cellWidget.setLayout(layout)
self.tableWidget.setCellWidget(0, 4, cellWidget)
I'd like to make part of the text of a label clickable, like an inline hyperlink on a website. I know how to make an individual label clickable, but I'm not sure how to only make part of the label clickable and still maintain a consistent format.
I've placed the code for my first attempt below and included an image of the output.
The two issues I see are the noticeable space between the labels (which even a QStretchItem at the end doesn't fix) and the issues with word wrapping.
Any help would be greatly appreciated. Thank you!
from PyQt5.QtWidgets import *
app = QApplication([])
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle('Title')
self.setGeometry(1200, 200, 350, 500)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# Dummy list to print
place_list = { '2000': 'An event happened.',
'2005': 'An event at {this place} happened long ago.',
'2010': 'Another event happened at {a different place}, but it was not fun.' }
# Initialize Grid of Notes
grid = QGridLayout()
# Create Headers for each column
grid.addWidget(QLabel('Date'), 0, 0)
grid.addWidget(QLabel('Note'), 0, 1)
index = 1
# Iterate through each entry in place_list
for year in place_list:
# Add index of entry (by year)
grid.addWidget(QLabel(year), index, 0)
# Get text of entry
note = place_list[year]
# Look for "{}" to indicate link
if '{' in note:
# Get location of link within the entry
start = note.find('{')
end = note.find('}')
# Create a label for the text before the link
lab_1 = QLabel(note[:start])
lab_1.setWordWrap(True)
# Create a label for the link
# NOTE: It's a QLabel for formatting purposes only
lab_2 = QLabel(note[start+1:end])
lab_2.setWordWrap(True)
# Create a label for the text after the link
lab_3 = QLabel(note[end+1:])
lab_3.setWordWrap(True)
# Combine the labels in one layout
note_lab = QHBoxLayout()
note_lab.addWidget(lab_1)
note_lab.addWidget(lab_2)
note_lab.addWidget(lab_3)
# Add the layout as the entry
grid.addLayout(note_lab, index, 1)
else:
# Create the label for the whole entry if no link indicator is found
note_lab = QLabel(note)
note_lab.setWordWrap(True)
grid.addWidget(note_lab, index, 1)
# Go to next row in grid
index += 1
self.layout.addLayout(grid)
window = MainWindow()
window.show()
app.exec_()
The best solution I believe is to subclass QLabel and override the mousePressEvent method.
def mousePressEvent(event):
# event.pos() or .x() and .y() to find the position of the click.
If you create a QRect in the area that you want in the initialization of your custom QLabel, you can easily check if the click is inside the rectangle by using the QRect.contains() method as well.
Other useful methods for this would be mouseReleaseEvent and mouseDoubleClickEvent.
And in general, when you are adding/changing functionality to widgets, look to subclass first.
I have a table view that display some SQLite data with a search filter associated to each query. The table view shows a query of the combination of the different filters. The filters are some QLineEdit located on top of the table in a QHBoxLayout. The QLineEdits are automatically resized with the size of the columns.
The problem here comes because the tableview is too big for being showed at once (it has 28 columns), so it has a scrollbar, and then I cannot resize automatically the QLineEdits to the size of the columns, as they are restricted to the size of the window. What I want to do is to create a QScrollArea that involves both the filters and the table, in a way the same scroll bar that moves the QTableView moves also the filter layout with it, which size is not restricted to the size of the window anymore.
I hope I explained myself clearly. Thanks a lot in advance!
Here it is my code at the moment, I've been trying for a while but I didn't find a satisfyings solution, either the size of the filters does not expand or the scroll bar only works for the table but not for the filters:
class ManualSearch(QtWidgets.QWidget):
"""Table in which the user can search filtering by any of its columns.
It also allows to open the source document directly."""
def __init__(self, query, header_names):
super(ManualSearch, self).__init__()
# Set up the model view architecture
self.model = QtSql.QSqlTableModel()
self.model.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange)
self.view = QtWidgets.QTableView()
self.view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.view.setModel(self.model)
self.view.setAlternatingRowColors(True)
self.view.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
self.view.resizeColumnsToContents()
self.view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
header = self.view.horizontalHeader()
header.setStretchLastSection(True)
header.setCascadingSectionResizes(True)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.sizeConstraint()
# Create a combined layout with the filters and the table
table_layout = QtWidgets.QVBoxLayout()
filter_layout = QtWidgets.QHBoxLayout()
self.filter_list = []
# We use a for loop to create the filters and link them
# to their correspondent columns
for i in range(len(header_names)):
filter_i = QtWidgets.QLineEdit()
filter_i.textChanged.connect(
lambda: self.update_query_filter(query)
)
header.sectionResized.connect(
lambda: self.resize(i, filter_i)
)
header.geometriesChanged.connect(
lambda: self.resize(i, filter_i)
)
filter_layout.addWidget(filter_i)
self.filter_list.append(filter_i)
filter_layout.setContentsMargins(25, 0, 10, 0)
table_layout.addLayout(filter_layout)
table_layout.addWidget(self.view)
self.source_button = QtWidgets.QPushButton('Open source document')
self.source_button.clicked.connect(self.open_source)
self.scroll = QtWidgets.QScrollArea()
self.scroll.setBackgroundRole(QtGui.QPalette.Light)
self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.scroll.setWidgetResizable(False)
self.scroll.setLayout(table_layout)
main_layout.addLayout(table_layout)
main_layout.addWidget(self.scroll)
main_layout.addWidget(self.source_button)
self.update_query_filter(query)
set_table_headers(self.model, header_names)
def resize(self, order, line):
"""Resize the width of the filter line edit to match the width of its
corresponding column"""
line.setFixedWidth(self.view.columnWidth(order))
Is it possible to have a multi-line text entry field with drop down options?
I currently have a GUI with a multi-line Text widget where the user writes some comments, but I would like to have some pre-set options for these comments that the user can hit a drop-down button to select from.
As far as I can tell, the Combobox widget does not allow changing the height of the text-entry field, so it is effectively limited to one line (expanding the width arbitrarily is not an option). Therefore, what I think I need to do is sub-class the Text widget and somehow add functionality for a drop down to show these (potentially truncated) pre-set options.
I foresee a number of challenges with this route, and wanted to make sure I'm not missing anything obvious with the existing built-in widgets that could do what I need.
Terry's feedback made it clear that there was no simple way to solve this, so I created a custom class which wraps a Text and a Button into a frame, with a Toplevel containing a Listbox spawned by the button's callback function. I added a couple of "nice-to-have" features, like option highlighting within the Listbox, and I mapped bindings of the main widget onto the internal Text widget to make it easier to work with. Please leave a comment if there's any glaring bad practices here; I'm definitely still pretty inexperienced! But I hope this helps anybody else who's looking for a multi-line combobox!
class ComboText(tk.Frame):
def __init__(self, parent=None, **kwargs):
super().__init__(parent)
self.parent = parent
self._job = None
self.data = []
self['background'] = 'white'
self.text = tk.Text(self, **kwargs)
self.text.pack(side=tk.LEFT, expand=tk.YES, fill='x')
symbol = u"\u25BC"
self.button = tk.Button(self,width = 2,text=symbol, background='white',relief = 'flat', command = self.showOptions)
self.button.pack(side=tk.RIGHT)
#pass bindings from parent frame widget to the inner Text widget
#This is so you can bind to the main ComboText and have those bindings
#apply to things done within the Text widget.
#This could also be applied to the inner button widget, but since
#ComboText is intended to behave "like" a Text widget, I didn't do that
bindtags = list(self.text.bindtags())
bindtags.insert(0,self)
self.text.bindtags(tuple(bindtags))
def showOptions(self):
#Get the coordinates of the parent Frame, and the dimensions of the Text widget
x,y,width,height = [self.winfo_rootx(), self.winfo_rooty(), self.text.winfo_width(), self.text.winfo_height()]
self.toplevel = tk.Toplevel()
self.toplevel.overrideredirect(True) #Use this to get rid of the menubar
self.listbox = tk.Listbox(self.toplevel,width=width, height =len(self.data))
self.listbox.pack()
#Populate the options in the listbox based on self.data
for s in self.data:
self.listbox.insert(tk.END,s)
#Position the Toplevel so that it aligns well with the Text widget
list_height = self.listbox.winfo_reqheight()
self.toplevel.geometry("%dx%d+%d+%d" % (width, list_height, x, y+height))
self.listbox.focus_force()
self.listbox.bind("<Enter>", self.ListboxHighlight)
self.listbox.bind("<Leave>",self.stopListboxHighlight)
self.listbox.bind("<Button-1>",self.selectOption)
self.toplevel.bind("<Escape>", self.onCancel)
self.toplevel.bind("<FocusOut>", self.onCancel)
def ListboxHighlight(self,*ignore):
#While the mouse is moving within the listbox,
#Highlight the option the mouse is over
x,y = self.toplevel.winfo_pointerxy()
widget = self.toplevel.winfo_containing(x,y)
idx = self.listbox.index("#%s,%s" % (x-self.listbox.winfo_rootx(),y-self.listbox.winfo_rooty()))
self.listbox.selection_clear(0,100) #very sloppy "Clear all"
self.listbox.selection_set(idx)
self.listbox.activate(idx)
self._job = self.after(25,self.ListboxHighlight)
def stopListboxHighlight(self,*ignore):
#Stop the recurring highlight function.
if self._job:
self.after_cancel(self._job)
self._job = None
def onCancel(self,*ignore):
#Stop callback function to avoid error once listbox destroyed.
self.stopListboxHighlight()
#Destroy the popup Toplevel
self.toplevel.destroy()
def selectOption(self,event):
x,y = [event.x,event.y]
idx = self.listbox.index("#%s,%s" % (x,y))
if self.data:
self.text.delete('1.0','end')
self.text.insert('end',self.data[idx])
self.stopListboxHighlight()
self.toplevel.destroy()
self.text.focus_force()
def setOptions(self,optionList):
self.data = optionList
#Map the Text methods onto the ComboText class so that
#the ComboText can be treated like a regular Text widget
#with some other options added in.
#This was necessary because ComboText is a subclass of Frame, not Text
def __getattr__(self,name):
def textMethod(*args, **kwargs):
return getattr(self.text,name)(*args, **kwargs)
return textMethod
if __name__ == '__main__':
root = tk.Tk()
ct = ComboText(root, width = 50, height = 3)
ct.pack()
ct.setOptions(['Option %d' % i for i in range (0,5)])
root.mainloop()
I don't think you are missing anything. Note that ttk.Combobox is a composite widget. It subclasses ttk.Entry and has ttk.Listbox attached.
To make multiline equivalent, subclass Text. as you suggested. Perhaps call it ComboText. Attach either a frame with multiple read-only Texts, or a Text with multiple entries, each with a separate tag. Pick a method to open the combotext and methods to close it, with or without copying a selection into the main text. Write up an initial doc describing how to operate the thing.
I'm making a table-like widget that displays an image, the file name, and two box-selection areas. I have two objects 'grid_row' & 'grid_table' (both using QGridLayout), grid_row being a single row and grid_table containing x number of grid_rows (I'm designing it like this because it's simply easier to keep track of my custom properties).
The tool looks like this
The final layout is a QVBoxLayout, then from top to bottom, I have QHBoxLayout(the one with a label and combobox), grid_row(for the headers 1,2,3), a scroll_area that contains the grid_table with each one being grid_rows. Lastly another QHBoxLayout for the buttons.
Each grid_row contains a 'image-widget', and two region labels(QLabel). The image widget contains a label(I used setPixmap for display) and a pushbutton. Here are my grid_row and image_widget classes:
class grid_row(QWidget):
def __init__(self, parent=None):
super().__init__()
#self.frame = frame_main()
self.grid_layout = QGridLayout()
self.grid_layout.setSpacing(50)
self.image_widget = image_widget()
self.grid_layout.addWidget(self.image_widget, 0, 0, 1, 1, Qt.AlignHCenter)
self.region_2 = QLabel('null')
self.grid_layout.addWidget(self.region_2, 0, 2, 1, 1, Qt.AlignHCenter)
self.setLayout(self.grid_layout)
self.region_1 = QLabel('null')
self.grid_layout.addWidget(self.region_1, 0, 1, 1, 1, Qt.AlignHCenter)
class image_widget(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.image_widget_layout = QHBoxLayout()
self.image_widget_label = QLabel()
self.image_widget_label.setPixmap(QPixmap('default.png').scaled(96, 54))
self.image_widget_layout.addWidget(self.image_widget_label)
self.img_btn = QPushButton()
self.img_btn.setEnabled(False)
self.img_btn.setText('Drag Here!')
self.image_widget_layout.addWidget(self.img_btn)
self.setLayout(self.image_widget_layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = QWidget()
layout = QVBoxLayout()
grid_row = grid_row()
layout.addWidget(grid_row)
btn = QPushButton('press')
btn.clicked.connect(lambda: grid_row.region_1.setText('[0,0,1920,1080]'))
layout.addWidget(btn)
widget.setLayout(layout)
scroll_area = QScrollArea()
scroll_area.setWidget(widget)
scroll_area.show()
sys.exit(app.exec_())
So currently, I've implemented events that allow me to drag images into the image_widget and click the push button to modify the two regions that are framed (format: [x1, y1, x2, y2]). The problem is that when I do that(e.g. region values go from 'null' to say '[20,20, 500, 500]', the image gets squished because now the labels are taking up more width.
I realize that some size policy needs to be set (and maybe other properties) but I don't know which property to use and on which widget. I want the image to remain the same. Maybe stretch out the width of each column for the grid_row?
To clarify, I want the label containing the pixmap to remain the same size (always 96*54) and fully displayed(not cropped or stretched) at all times.
I've provided the a simplified executable code to display my problem, the classes are the same as my code, I just only put grid_row inside the scroll_area and added a button to change one of the values of the region to simulate the situation. Can provide additional code if needed. Thanks in advance!
Wow sometimes the answer is really one extra line of code...
So the documentation mentions that QScrollArea by default honors the size of its widget. Which is why when I changed the region (to a value that's wider/ more text) the widget does not auto adjust.
I needed to add
scroll_area.setWidgetResizable(True)
to allow the widget to resize wider thus prompting the scroll bars to appear. This way my pixmap image doesn't get cropped from not having enough space.
The easiest way would be to add size constraints to the label before adding to the layout
self.image_widget_label.adjustSize()
self.image_widget_label.setFixedSize(self.image_widget_label.size())
self.image_widget_layout.addWidget(self.image_widget_label)
adjustSize would resize the label depending on the contents.
The more difficult way is to answer the questions :
"when I change the size of the overall window, how do I want this
particular item to behave? When the window is at its minimal size,
which items do I want hidden or out of view? When the window is full
size, where do I want empty spots?"
To answer these better read a bit on Qt Layout management