I'm working on a GridLayout (here called CostosInput) that adds TextInputs to itself when a button is pressed. However, when it does, the layout doesn't resize the widgets around it, which causes it to overlap once a certain amount of TextInput widgets are added. How can I fix that?
It is part of another GridLayout, which has one column and is contained in a ScrollView. The widget below it (the one it overlaps with) is a Label.
I've tried looking in the Kivy docs, but the only stuff I've found is related to the size of Layouts and Widgets, and not necessarily their placement. I thought it'd be something like the layout_hint_with_bounds(sh_sum, available_space, min_bounded_size, sh_min_vals, sh_max_vals, hint) (link to the Kivy docs), but that's an internal function. I apologize if something doesn't conform to convention.
Here's the code for the CostoInput class.
class CostosInput(GridLayout):
def __init__(self, *args, **kwargs):
super(CostosInput, self).__init__()
self.cols = 2
self.spacing = 5
self.padding = 5
self.size_hint_y = 0.2
# used in add_lines()
self.count = 1
add = Orange_Button(text='AGREGAR COSTO')
add.bind(on_press=self.add_lines)
rem = Orange_Button(text='QUITAR COSTO')
rem.bind(on_press=self.del_lines)
self.add_widget(add)
self.add_widget(rem)
self.add_widget(Orange_Label(text='concepto'))
self.add_widget(Orange_Label(text='costo'))
self.add_lines()
# this function is adds TextInputs
# when the add button is pressed
def add_lines(self, *args, **kwargs):
text = Normal_TI()
text.text = 'costo ' + str(self.count)
text.bind(text=self.change)
flot = FloatInput()
flot.bind(text=self.change)
self.add_widget(text)
self.add_widget(flot)
self.size_hint_y += 0.1
self.count += 1
# this has nothing to do with the ui
def del_lines(self, *args, **kwargs):
for x in range(0, len(self.children) - 4, 2):
if len(self.children) > 4:
try:
if self.children[x].focus or self.children[x + 1].focus:
self.remove_widget(self.children[x])
self.remove_widget(self.children[x])
except Exception as err:
print('del lines: no se pudieron quitar,'
'err {}'.format(err))
# this also has nothing to do with the ui
def change(self, value, *args):
dicto = dict()
for x in range(0, len(self.children) - 4, 2):
dicto[self.children[x + 1].text] = self.children[x].text
print(dicto)
producto.costos = dicto
Here's the code for the class the label is instantiated with. The text attribute is the only part that changes.
class Orange_Label(Label):
def __init__(self, *args, **kwargs):
super(Orange_Label, self).__init__(*args, **kwargs)
self.size_hint_y = None
self.size = (self.width, 10)
self.color = (0.95668627451, 0.6941176471, 0.5137254902, 1)
Related
I have a RadioButtonWidget class that receives a list of names (button_list) and a QtWidgets.QGroupBox (radio_group_box) and creates a radio button for each name. The problem I have is that after creating the buttons, I cannot change them. That is if I call the class again with another list of names, nothing changes. I need to create a function inside my class to remove any existing radio buttons so that I can add a new list inside it.
I tried to do radio_group_box.deleteLater() outside the class but this removes the whole box.
class RadioButtonWidget(QtWidgets.QWidget):
def __init__(self, radio_group_box, button_list):
super().__init__()
self.radio_group_box = radio_group_box
self.radio_button_group = QtWidgets.QButtonGroup()
#create the radio buttons
self.radio_button_list = []
for each in button_list:
self.radio_button_list.append(QtWidgets.QRadioButton(each))
if button_list != []:
#set the default checked item
self.radio_button_list[0].setChecked(True)
#create layout for radio buttons and add them
self.radio_button_layout = QtWidgets.QVBoxLayout()
# add buttons to the layout and button group
counter = 1
for each in self.radio_button_list:
self.radio_button_layout.addWidget(each)
self.radio_button_group.addButton(each)
self.radio_button_group.setId(each,counter)
counter += 1
# add radio buttons to the group box
self.radio_group_box.setLayout(self.radio_button_layout)
def selected_button(self):
return self.radio_button_group.checkedId()
Instead of removing the radio buttons, you can create a whole new radio button layout and set it for the group box exactly as you did in the constructor. Here is an example where the function set_group_box_buttons will remove the existing layout from radio_group_box (which is done by setting it to a temp widget), and add a new one with the new buttons.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class RadioButtonWidget(QWidget):
def __init__(self, radio_group_box, button_list):
super().__init__()
self.radio_group_box = radio_group_box
self.set_group_box_buttons(button_list)
grid = QGridLayout(self)
grid.addWidget(self.radio_group_box)
def selected_button(self):
return self.radio_button_group.checkedId()
def set_group_box_buttons(self, button_list):
self.radio_button_group = QButtonGroup()
self.radio_button_list = [QRadioButton(x) for x in button_list]
if button_list:
self.radio_button_list[0].setChecked(True)
if self.radio_group_box.layout():
QWidget().setLayout(self.radio_group_box.layout())
self.radio_button_layout = QVBoxLayout()
for i, v in enumerate(self.radio_button_list):
self.radio_button_layout.addWidget(v)
self.radio_button_group.addButton(v)
self.radio_button_group.setId(v, i)
self.radio_group_box.setLayout(self.radio_button_layout)
class Template(QWidget):
def __init__(self):
super().__init__()
self.rbw = RadioButtonWidget(QGroupBox('Radio Buttons'), ['Radio 1', 'Radio 2', 'Radio 3'])
self.box = QLineEdit()
self.box.returnPressed.connect(self.replace_buttons)
grid = QGridLayout(self)
grid.addWidget(self.rbw, 0, 0)
grid.addWidget(self.box, 0, 1)
def replace_buttons(self):
self.rbw.set_group_box_buttons(self.box.text().split(', '))
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = Template()
gui.show()
sys.exit(app.exec_())
To demonstrate, I added a QLineEdit which will update the names when you press enter. Before:
After:
There's a conceptual error in your code: you are creating a new RadioButtonGroup, which is a widget, but you are not using it.
As long as each group box will only contain the radio buttons, there is no need to create a new widget (especially if you're not actually using it); you just have to create a layout if the groupbox doesn't have one yet.
There are at least two possible approaches to your question.
For both of them I always use existing radios if possible, to avoid unnecessary object destruction each time the options change, so that they are removed only when the number of options decreases. This also avoids unnecessary layout updates (especially if the number of options is the same).
I also kept the logical "interface" consistent, providing the same method and behavior of update_options(groupBox, options).
QObject based group
With this implementation, I'm creating an object that acts as an interface responsible of creating a QButtonGroup and setting the options, while also providing signals for the state change or the current checked radio.
class RadioButtonGroup(QtCore.QObject):
optionToggled = QtCore.pyqtSignal(object, int, bool)
optionChanged = QtCore.pyqtSignal(object, int)
def __init__(self, radio_group_box, button_list):
super().__init__()
self.groupBox = radio_group_box
layout = radio_group_box.layout()
self.buttonGroup = QtWidgets.QButtonGroup(self)
self.buttonGroup.buttonToggled[int, bool].connect(self.changed)
if layout is None:
layout = QtWidgets.QVBoxLayout(radio_group_box)
for i, text in enumerate(button_list, 1):
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
self.buttonGroup.addButton(radio, i)
def button(self, id):
return self.buttonGroup.button(id)
def changed(self, i, state):
self.optionToggled.emit(self, i, state)
if state:
self.optionChanged.emit(self, i)
def selected_button(self):
return self.buttonGroup.checkedId()
def update_options(self, button_list):
layout = self.groupBox.layout()
# this method will keep the current checked radio as checked, if you want
# to reset it everytime, just uncomment the next commented lines
#self.buttonGroup.setExclusive(False)
for i, text in enumerate(button_list, 1):
radio = self.buttonGroup.button(i)
if radio:
#radio.setChecked(False)
radio.setText(text)
else:
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
self.buttonGroup.addButton(radio, i)
#self.buttonGroup.setExclusive(True)
if len(button_list) == len(self.buttonGroup.buttons()):
return
# there are more radios than needed, remove them
for radio in self.buttonGroup.buttons():
id = self.buttonGroup.id(radio)
if id > i:
self.buttonGroup.removeButton(radio)
radio.deleteLater()
class ObjectBased(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('buttongroup.ui', self)
self.pushButton.clicked.connect(self.setOptions)
self.groupBoxes = self.groupBox1, self.groupBox2, self.groupBox3
self.radioButtonGroups = []
for box in self.groupBoxes:
group = RadioButtonGroup(box,
['Option {}'.format(o + 1) for o in range(randrange(1, 10))])
self.radioButtonGroups.append(group)
group.optionChanged.connect(self.optionChanged)
def setOptions(self):
buttonGroup = self.radioButtonGroups[self.comboBox.currentIndex()]
options = ['Option {}'.format(o + 1) for o in range(self.spinBox.value())]
buttonGroup.update_options(options)
def optionChanged(self, radioButtonGroup, id):
groupBox = radioButtonGroup.groupBox
print('{} checked {} ({})'.format(
groupBox.title(), id, radioButtonGroup.button(id).text()))
Self contained
In this mode, the logic is all within the window class. While this approach is slightly simpler than the other one, we're missing an unique "interface", which might be useful for access from external objects instead.
class SelfContained(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('buttongroup.ui', self)
self.pushButton.clicked.connect(self.setOptions)
self.radioButtonGroups = []
for g, groupBox in enumerate((self.groupBox1, self.groupBox2, self.groupBox3)):
buttonGroup = QtWidgets.QButtonGroup(self)
self.radioButtonGroups.append((groupBox, buttonGroup))
buttonGroup.buttonToggled[int, bool].connect(lambda id, state, g=g: self.optionChanged(g, id, state))
self.update_options(g, ['Option {}'.format(o + 1) for o in range(randrange(1, 10))])
def update_options(self, groupId, button_list):
groupBox, buttonGroup = self.radioButtonGroups[groupId]
layout = groupBox.layout()
if layout is None:
layout = QtWidgets.QVBoxLayout(groupBox)
# as above...
#buttonGroup.setExclusive(False)
for i, text in enumerate(button_list, 1):
radio = buttonGroup.button(i)
if radio:
#radio.setChecked(False)
radio.setText(text)
else:
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
buttonGroup.addButton(radio, i)
#buttonGroup.setExclusive(True)
if len(button_list) == len(buttonGroup.buttons()):
return
for radio in buttonGroup.buttons():
id = buttonGroup.id(radio)
if id > i:
buttonGroup.removeButton(radio)
radio.deleteLater()
def setOptions(self):
groupId = self.comboBox.currentIndex()
options = ['Option {}'.format(o + 1) for o in range(self.spinBox.value())]
self.update_options(groupId, options)
def optionChanged(self, groupId, id, state):
if state:
groupBox, buttonGroup = self.radioButtonGroups[groupId]
print('{} checked {} ({})'.format(groupBox.title(), id, buttonGroup.button(id).text()))
I'm trying to build a chessboard consisting of buttons.
I created 3 widgets in one line. There are labels outside (filling) and inside I want to put a chessboard.
I would like it to always occupy 90% of the screen width and automatically adjust its height so that it always remains a square. It would also be necessary to set the buttons always to be squares but I also can't handle it. Can You help me?
class ChessBoard(GridLayout):
def __init__(self, **kwargs):
super(ChessBoard, self).__init__(**kwargs)
self.cols = 8
for i in range(64):
self.cell = Button(text="", size_hint_y=self.height/8, height=self.width/8)
self.add_widget(self.cell)
class ChessBoardContainer(GridLayout):
def __init__(self, **kwargs):
super(ChessBoardContainer, self).__init__(**kwargs)
self.orientation='horizontal'
self.cols=3
self.lab1 = Label(text="1")
self.add_widget(self.lab1)
self.board = ChessBoard()
self.add_widget(self.board)
self.lab2 = Label(text="2")
self.add_widget(self.lab2)
class CombWidget(BoxLayout):
def __init__(self, **kwargs):
super(CombWidget, self).__init__(**kwargs)
self.orientation='vertical'
self.but1 = Button(text="But1", font_size=40)
self.add_widget(self.but1)
self.chessb = ChessBoardContainer()
self.add_widget(self.chessb)
self.but2 = Button(text="But2", font_size=40)
self.add_widget(self.but2)
class MyPaintApp(App):
def build(self):
return CombWidget()
Right now this is my result:
I would like to get something like this (Paint master ;) ). Maybe it could be done without this labels?
To make buttons be squares you just have to set the height and width of GridLayout cells, and you are trying to do it with size_hint. Try this:
from kivy.core.window import Window
class ChessBoard(GridLayout):
def __init__(self, **kwargs):
super(ChessBoard, self).__init__(**kwargs)
self.cols = 8
winsize = Window.size
sizedict = {}
# to set width and height of GridLayout cells, you should make a dict, where the key is col's/row's number and the value is size
for i in range(self.cols):
sizedict[i] = winsize[0]/8 #or you can divide it by 10 for example to have some black filling on the sides
# and then simply do this
self.cols_minimum = sizedict
self.rows_minimum = sizedict
This code produces buttons that look fairly square to me. If you plan to use images for your chess pieces, the buttons will conform to the size of those.
from tkinter import Tk, Button
window = Tk ()
squares = []
index = 0
for x in range (8) :
for y in range (8) :
squares.append (Button (window, width = 7, height = 4))
squares [index].grid (row = x, column = y)
index += 1
window.mainloop ()
In Kivy manual is example for using FileChooser with kivy language. I want to use FileChooser only in python code. When I mark directory by mouse, press button Select Dir and actually value is in variable FileChooser.path. Selection without using this button has not result.
In kv file from example is used event on_selection, I binded this event with my function, but without effect.
My questions:
How do I get value to path with only mouse using?
Which class use event on_selection?
Thank You!
class Explorer(BoxLayout):
def __init__(self, **kwargs):
super(Explorer,self).__init__(**kwargs)
self.orientation = 'vertical'
self.fichoo = FileChooserListView(size_hint_y = 0.8)
self.add_widget(self.fichoo)
control = GridLayout(cols = 5, row_force_default=True, row_default_height=35, size_hint_y = 0.14)
lbl_dir = Label(text = 'Folder',size_hint_x = None, width = 80)
self.tein_dir = TextInput(size_hint_x = None, width = 350)
bt_dir = Button(text = 'Select Dir',size_hint_x = None, width = 80)
bt_dir.bind(on_release =self.on_but_select)
self.fichoo.bind(on_selection = self.on_mouse_select)
control.add_widget(lbl_dir)
control.add_widget(self.tein_dir)
control.add_widget(bt_dir)
self.add_widget(control)
return
def on_but_select(self,obj):
self.tein_dir.text = str(self.fichoo.path)
return
def on_mouse_select(self,obj):
self.tein_dir.text = str(self.fichoo.path)
return
def on_touch_up(self, touch):
self.tein_dir.text = str(self.fichoo.path)
return super().on_touch_up(touch)
return super().on_touch_up(touch)
Few changes need to be done.
There's no such event on_selection, there's property selection in FileChooserListView. You can use functions on_<propname> inside class that has these property, but when you use bind, you should use only bind(<propname>=.
Second thing is that by default as you can see in the doc selection contains list of selected files, not directories. To make directories actually selctable, you should change dirselect property to True.
Last on is on_mouse_select signature: property fires with it's value, you should count that.
Summary changes are:
self.fichoo.dirselect = True
self.fichoo.bind(selection = self.on_mouse_select)
# ... and
def on_mouse_select(self, obj, val):
After that you would be do the same as with button.
If you want to fill input with not actually path you're in, but selected path, you can do following:
def on_touch_up(self, touch):
if self.fichoo.selection:
self.tein_dir.text = str(self.fichoo.selection[0])
return super().on_touch_up(touch)
I have created a RelativeLayout subclass that positions its children in a grid by supplying them positions in the code (Python, not kv file). It works, but items are placed some 25 pixels to the upper-right from the position of layout itself, as shown by the canvas block. Python code for Layout subclass:
class RLMapWidget(RelativeLayout):
def __init__(self, map=None, **kwargs):
super(FloatLayout, self).__init__(**kwargs)
# Connecting to map, factories and other objects this class should know about
self.tile_factory = TileWidgetFactory()
self.map = map
# Initializing tile widgets for BG layer and adding them as children
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
tile_widget = self.tile_factory.create_tile_widget(self.map.get_item(layer='bg',
location=(x, y)))
# tile_widget.pos = (50*x, 50*y)
tile_widget.pos = self._get_screen_pos((x, y))
self.add_widget(tile_widget)
# Initializing widgets for actor layers
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
if self.map.has_item(layer='actors', location=(x, y)):
actor_widget = self.tile_factory.create_actor_widget(self.map.get_item(layer='actors',
displayed location=(x, y)))
actor_widget.pos=(50*x, 50*y)
self.add_widget(actor_widget)
# Map background canvas. Used solely to test positioning
with self.canvas.before:
Color(0, 0, 1, 1)
self.rect = Rectangle(size = self.size, pos=self.pos)
self.bind(pos=self.update_rect, size=self.update_rect)
# Initializing keyboard bindings and key lists
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
self._keyboard.bind(on_key_down=self._on_key_down)
# The list of keys that will not be ignored by on_key_down
self.used_keys=['w', 'a', 's', 'd']
def redraw_actors(self):
for actor in self.map.actors:
actor.widget.pos = self._get_screen_pos(actor.location)
def _get_screen_pos(self, location):
"""
Return screen coordinates (in pixels) for a given location
:param location: int tuple
:return: int tuple
"""
return (location[0]*50, location[1]*50)
# Keyboard-related methods
def _on_key_down(self, keyboard, keycode, text, modifiers):
"""
Process keyboard event and make a turn, if necessary
:param keyboard:
:param keycode:
:param text:
:param modifiers:
:return:
"""
if keycode[1] in self.used_keys:
self.map.process_turn(keycode)
self.redraw_actors()
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_key_down)
self._keyboard = None
def update_rect(self, pos, size):
self.rect.pos = self.pos
self.rect.size = self.size
class CampApp(App):
def build(self):
root = FloatLayout()
map_factory = MapFactory()
map = map_factory.create_test_map()
map_widget = RLMapWidget(map=map,
size=(map.size[0]*50, map.size[1]*50),
size_hint=(None, None))
root.add_widget(map_widget)
return root
if __name__ == '__main__':
CampApp().run()
Factory class that makes tiles:
class TileWidgetFactory(object):
def __init__(self):
pass
def create_tile_widget(self, tile):
tile.widget = Image(source=tile.image_source,
size_hint=(None, None))
return tile.widget
def create_actor_widget(self, actor):
actor.widget = Image(source='Tmp_frame_black.png',
size_hint=(None, None))
return actor.widget
Okay, solved it myself. It turns out that if I supply the size to child widgets in factory, they are positioned properly. Although it solves the problem I have, I'd still be grateful if someone can explain where this quirk does come from.
I have this class to create a Statusbar:
class Statusbar(Canvas):
'''Creates a statusbar widget'''
def __init__(self, master = None, **options):
if not master: master = Tk()
self.master, self.options = master, options
self.barFill, self.addText, self.value = self.options.get('barFill', 'red'), self.options.get('addText', True), 0
for option in ('barFill', 'addText'):
if option in self.options: del self.options[option]
Canvas.__init__(self, master, **self.options)
self.offset = self.winfo_reqwidth() / 100
self.height = self.winfo_reqwidth()
if self.addText: self.text = self.create_text(self.winfo_reqwidth()/2, self.winfo_reqheight()/2, text = '0%')
self.bar = self.create_rectangle(0, 0, self.value, self.height, fill = self.barFill)
def setValue(self, value):
'''Sets the value of the status bar as a percent'''
self.value = value * self.offset
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
def change(self, value):
'''Changes the value as a percent'''
self.value += (value * self.offset)
self.coords(self.bar, 0, 0, self.value, self.height)
if self.addText: self.itemconfigure(self.text, text = str(self.value/self.offset) + '%')
My issue is that the text is always drawn under rectangle. So when the rectangle reaches the text, you can't see the text anymore. How can I fix this? Thanks in advance.
The fact that one object sits atop another is called the stacking order. By default, objects created later have a higher stacking order than those that were created earlier. So, one solution is to draw the rectangle and then draw the text.
You can also move things up or down the stacking order using the lift and lower commands of a canvas. You must give it an id or tag of what you want to lift or lower, and optionally an id or tag of the object you want the first object(s) to be above or below.
So, for example, you could raise the text above the rectangle like this:
self.lift(self.text, self.bar)
If you want to get really fancy, you can create the notion of layers. I gave an example in another answer, here: https://stackoverflow.com/a/9576938/7432
In my programming class, we said put whatever text you don't want to be blocked drawn last. So put the text at the bottom of what function you are using to draw with