I'm working on a PyQt5 and Qml application. The application displays 2 QLineEdits, 2 QPushButton and a map that is given from my Qml. One of the functionalities of the application is parsing(in python) through an XML-List with titles and coordinates and adding pins on the coordinates on the map. My problem is now that it only displays the last coordinate. I realized that it's because I only change the property of the pin Item in Qml and not create a new one. My Question is now: Is it possible to create MapQuickItems in python?
As I commented #derM you only have to create a model and expose it to your QML, there is no documentation for PyQt, but we can take as reference to the C ++ documentation since the concepts are similar. As a sample of this I have created in the following model that contains the information of a marker, in this case consider the position and color of the marker.
class MarkerItem(object):
def __init__(self, position, color=QColor("red")):
self._position = position
self._color = color
def position(self):
return self._position
def setPosition(self, value):
self._position = value
def color(self):
return self._color
def setColor(self, value):
self._color = value
class MarkerModel(QAbstractListModel):
PositionRole = Qt.UserRole + 1
ColorRole = Qt.UserRole + 2
_roles = {PositionRole: QByteArray(b"markerPosition"), ColorRole: QByteArray(b"markerColor")}
def __init__(self, parent=None):
QAbstractListModel.__init__(self, parent)
self._markers = []
def rowCount(self, index=QModelIndex()):
return len(self._markers)
def roleNames(self):
return self._roles
def data(self, index, role=Qt.DisplayRole):
if index.row() >= self.rowCount():
return QVariant()
marker = self._markers[index.row()]
if role == MarkerModel.PositionRole:
return marker.position()
elif role == MarkerModel.ColorRole:
return marker.color()
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
if index.isValid():
marker = self._markers[index.row()]
if role == MarkerModel.PositionRole:
marker.setPosition(value)
if role == MarkerModel.ColorRole:
marker.setColor(value)
self.dataChanged.emit(index, index)
return True
return QAbstractListModel.setData(self, index, value, role)
def addMarker(self, marker):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._markers.append(marker)
self.endInsertRows()
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
return QAbstractListModel.flags(index)|Qt.ItemIsEditable
def moveRandom(self, ix):
ind = self.index(ix, 0)
current_pos = self.data(ind, MarkerModel.PositionRole)
next_pos = current_pos + 0.002*QPointF(random() - 0.5, random() - 0.5)
self.setData(ind, next_pos, MarkerModel.PositionRole)
self.setData(ind, QColor(randint(0, 255), randint(0, 255), randint(0, 255)), MarkerModel.ColorRole)
And then used in the following example:
main.py
if __name__ == "__main__":
import sys
QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
model = MarkerModel()
model.addMarker(MarkerItem(QPointF(-12.0464, -77.0428), QColor("black")))
model.addMarker(MarkerItem(QPointF(-12.0340, -77.0428), QColor("red")))
model.addMarker(MarkerItem(QPointF(-12.0440, -77.0280), QColor("#9f5522")))
context = engine.rootContext()
context.setContextProperty('markerModel', model)
engine.load(QUrl.fromLocalFile("main.qml"))
if len(engine.rootObjects()) == 0:
sys.exit(-1)
engine.quit.connect(app.quit)
timer = QTimer(engine)
timer.timeout.connect(lambda: model.moveRandom(0))
timer.start(100)
sys.exit(app.exec_())
main.qml
import QtQuick 2.0
import QtQuick.Window 2.0
import QtLocation 5.5
import QtPositioning 5.5
Window {
visible: true
title: "Python OSM"
width: 640
height: 480
property int marker_size: 16
Map {
id: map
anchors.fill: parent
plugin: Plugin {
name: "osm"
}
center: QtPositioning.coordinate(-12.0464, -77.0428)
zoomLevel: 14
MapItemView {
model: markerModel
delegate: MapQuickItem{
anchorPoint: Qt.point(2.5, 2.5)
coordinate: QtPositioning.coordinate(markerPosition.x, markerPosition.y)
zoomLevel: 0
sourceItem: Rectangle{
width: marker_size
height: marker_size
radius: marker_size/2
border.color: "white"
color: markerColor
border.width: 1
}
}
}
}
}
Obtaining what is shown in the following image:
Note: I added the moveRandom method to show you how to update the position of the markers, as you will see when executing the example a marker randomly moves
The complete example can be found in the following link
Related
I have QLineEdit in which I wanted to add a clear button at the end of it. I enabled clear button in QLineEdit, it was working fine. I need to add a custom clear button at the end of the QLineEdit, so I used addAction() of QLineEdit and added my custom icon. The problem is that I can't find a solution to increase the size, I tried increasing the image size and it's not working.
class TextBox(QFrame):
def __init__(self, parent):
super(TextBox, self).__init__(parent=parent)
self.setObjectName("textBox")
self.isActive = False
self.lineEdit = QLineEdit()
self.lineEdit.addAction(QIcon("assets/icons/clear#3x.png"), QLineEdit.TrailingPosition)
A QIcon does not have a specific size, as it's only "decided" by the widget that uses it. While most widgets that use icons have a iconSize property, the icons of actions in a QLineEdit are shown in a different way.
Up until Qt 5.11 (excluded), the size was hardcoded to 16 pixels if the line edit was smaller than 34 pixels or 32 if it was taller.
Starting from Qt 5.11 the size is retrieved using the style (through pixelMetric()), and this can be overridden using a proxy style:
class Proxy(QtWidgets.QProxyStyle):
def pixelMetric(self, metric, opt=None, widget=None):
if (metric == self.PM_SmallIconSize and
isinstance(widget, QtWidgets.QLineEdit)):
size = widget.property('iconSize')
if size is not None:
return size
return widget.fontMetrics().height()
return super().pixelMetric(metric, opt, widget)
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setProperty('iconSize', 64)
# ...
For previous versions of Qt, though, things are a bit tricky. The only solution I came up with is to install event filters on all QToolButton that are children of the line edit (every action uses an internal QToolButton, including the clear action), manually set their geometry (required for correct click actions) and paint it in the event filter.
The following includes the proxystyle implementation in case the current version correctly supports it as explained before:
from PyQt5 import QtWidgets, QtCore, QtGui
if int(QtCore.QT_VERSION_STR.split('.')[1]) > 11:
IconSizeFix = False
else:
IconSizeFix = True
class Proxy(QtWidgets.QProxyStyle):
def pixelMetric(self, metric, opt=None, widget=None):
if (metric == self.PM_SmallIconSize and
isinstance(widget, QtWidgets.QLineEdit)):
size = widget.property('iconSize')
if size is not None:
return size
return widget.fontMetrics().height()
return super().pixelMetric(metric, opt, widget)
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setProperty('iconSize', 64)
self.setClearButtonEnabled(True)
self.addAction(QtGui.QIcon("icon.png"), self.TrailingPosition)
font = self.font()
font.setPointSize(48)
self.setFont(font)
def checkButtons(self):
for button in self.findChildren(QtWidgets.QToolButton):
button.installEventFilter(self)
def actionEvent(self, event):
super().actionEvent(event)
if IconSizeFix:
self.checkButtons()
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Paint:
if (source.defaultAction().objectName() == '_q_qlineeditclearaction' and
not self.text()):
return True
qp = QtGui.QPainter(source)
state = QtGui.QIcon.Disabled
if source.isEnabled():
state = QtGui.QIcon.Active if source.isDown() else QtGui.QIcon.Normal
iconSize = QtCore.QSize(*[self.property('iconSize')] * 2)
qp.drawPixmap(source.rect(), source.icon().pixmap(
self.windowHandle(), iconSize, state, QtGui.QIcon.Off))
return True
return super().eventFilter(source, event)
def resizeEvent(self, event):
if not IconSizeFix:
return
self.checkButtons()
buttons = self.findChildren(QtWidgets.QToolButton)
if not buttons:
return
left = []
right = []
center = self.rect().center().x()
for button in buttons:
geo = button.geometry()
if geo.center().x() < center:
left.append(button)
else:
right.append(button)
left.sort(key=lambda x: x.geometry().x())
right.sort(key=lambda x: x.geometry().x())
iconSize = self.property('iconSize')
margin = iconSize / 4
top = (self.height() - iconSize) / 2
leftMargin = rightMargin = 0
if left:
x = margin
leftEdge = left[-1].geometry().right()
for button in left:
geo = QtCore.QRect(x, top, iconSize, iconSize)
button.setGeometry(geo)
x += iconSize + margin
leftMargin = x - leftEdge - margin
if right:
rightEdge = self.width() - margin
x = rightEdge - len(right) * iconSize - (len(right) - 1) * margin
rightMargin = self.width() - rightEdge + margin
for button in right:
geo = QtCore.QRect(x, top, iconSize, iconSize)
button.setGeometry(geo)
x += iconSize + margin
self.setTextMargins(leftMargin, 0, rightMargin, 0)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle(Proxy())
w = LineEdit()
w.show()
sys.exit(app.exec_())
Consider the following:
using the pre-5.11 workaround the positioning is not pixel-perfect, I tried to mimic what QLineEdit does to keep the code as simple as possible;
the painting is not exactly the same, most importantly the icon is missing the "highlight" shade when clicked, and if the style uses fade in/out effects for the clear button those effects won't be available;
the QProxyStyle method also affects the sizeHint of the QLineEdit, so it can not be smaller than the icon size, so use it with care;
I should display some rects with qml.
These Rectangle{} the positiopn of those depend on x=array[i][0] and y=array[i][1], and the quantity of those depend on array.lenght().
array in qml should be equal to self.__rectanglePos in Python
So I need a way to draw Rectangle in a different position and in different amount that depends array.
how can I do that?
_____.py
class NN(QObject):
def __init__(self):
QObject.__init__(self)
self.__rectanglePos = [] #each element contains:[x,y,orientation 360°]
#signals
rectanglePosChanged = Signal(type(self.__rectanglePos))
#Slot()
def rects(self):
self.rectanglePosChanged.emit(self.__rectanglePos)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(NN, 'gnn', 1, 0, 'NN')
engine.load(QUrl.fromLocalFile(gnn.qml))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
this is not working in qml:
________.qml
#[...]
import gnn 1.0
ApplicationWindow {
ListView{
id: rect
function f(){
for (var i = 0; i < array.lenght(); ++i){
Rectangle {
x: array[i][0]
y: array[i][1]
}
}
}
}
MouseArea{
onClicked: rects()
}
NN{
id: nn
signal getRectangles(var array) // not working
Component.onCompleted: {
nn.rectanglePosChanged.connect(getRectangles)
}
}
Connections {
target: nn
onGetRectangles: {
}
}
}
Your code has many errors, for example you want to access an attribute of the class: self.__rectanglePos) which only makes sense when creating an object from a part of the code that is executed before creating the class: rectanglePosChanged = Signal(type(self.__rectanglePos)), so I will avoid pointing out what your errors are except those necessary for my solution.
If you want to send information as a list where you add, replace or remove items then it is better to use a model as it will notify you in view of the changes. In this case for simplicity I use a QStandardItemModel, so each part of the element as "x" and "y" will be accessed through roles.
In the case of QML, as you want to place Rectangles in any position, using ListView is not the right option since this view restricts the position, instead you must use a Repeater.
main.py
import os
import sys
from PySide2 import QtCore, QtGui, QtQml
XRole = QtCore.Qt.UserRole + 1000
YRole = QtCore.Qt.UserRole + 1001
AngleRole = QtCore.Qt.UserRole + 1002
class RectangleManager(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.m_model = QtGui.QStandardItemModel(self)
roles = {XRole: b"x", YRole: b"y", AngleRole: b"angle"}
self.m_model.setItemRoleNames(roles)
#QtCore.Property(QtCore.QObject, constant=True)
def model(self):
return self.m_model
#QtCore.Slot(float, float, float)
def add_rectangle(self, x, y, angle):
it = QtGui.QStandardItem()
it.setData(x, XRole)
it.setData(y, YRole)
it.setData(angle, AngleRole)
self.model.appendRow(it)
#QtCore.Slot()
def clear_rectangles(self):
self.model.clear()
def main(args):
app = QtGui.QGuiApplication(args)
manager = RectangleManager()
import random
for _ in range(10):
manager.add_rectangle(
random.randint(0, 500), random.randint(0, 500), random.randint(0, 360)
)
engine = QtQml.QQmlApplicationEngine()
engine.rootContext().setContextProperty("rectangle_manager", manager)
current_dir = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(current_dir, "main.qml")
engine.load(QtCore.QUrl.fromLocalFile(filename))
if not engine.rootObjects():
return -1
ret = app.exec_()
return ret
if __name__ == "__main__":
sys.exit(main(sys.argv))
main.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
Repeater{
model: rectangle_manager.model
Rectangle{
x: model.x
y: model.y
rotation: model.angle
width: 100
height: 100
color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
}
}
}
I have created subclass of QTreeWidget and QTreeWidgetItem where I am trying to highlight the new added items (where the text will be colored red).
The tree hierarchy that I have adopted is as follows:
|-- parent
|--|-- child
|--|-- child
The tree widget is initially populated from a dictionary.
To get the difference, I did it by converting the current hierarchy in the tree widget into a dictionary and have it compared against the initial dictionary that it was populated with.
However if I add in a new child to an existing parent in which the name of the new child already existed in another parent, the same method as mentioned above does not works, as it will colored the first result that it find.
To replicate:
select menuB
right mouse click > add new sub item
input name: a101
hit "Highlight Diff." button
a101 child item under menuA is highlighted instead of the one in menuB
What would be the best way to go in getting the index of newly added child(ren)?
Thank you for any replies.
P.S: If anyone has better suggestion for the parent highlighting, please feel free to chip in.
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
def __init__(self, widget=None, text=None, is_tristate=False):
super(CustomTreeWidgetItem, self).__init__(widget)
self.setText(0, text)
if is_tristate:
# Solely for the Parent item
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsTristate
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
else:
self.setFlags(
self.flags()
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsUserCheckable
)
self.setCheckState(0, QtCore.Qt.Unchecked)
def setData(self, column, role, value):
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if (role == QtCore.Qt.CheckStateRole and
state != self.checkState(column)):
tree_widget = CustomTreeWidget()
if tree_widget is not None:
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_custom_menu)
def show_custom_menu(self):
base_node = self.selectedItems()[0]
qmenu = QtGui.QMenu(self)
remove_action = QtGui.QAction("Remove item", self)
remove_action.triggered.connect(self.remove_selected_item)
qmenu.addAction(remove_action)
# The following options are only effected for top-level items
# top-level items do not have `parent()`
if not base_node.parent():
add_new_child_action = QtGui.QAction("Add new sub item", self)
add_new_child_action.triggered.connect(
partial(self.adds_new_child_item, base_node)
)
# qmenu.addAction(add_new_child_action)
qmenu.insertAction(remove_action, add_new_child_action)
qmenu.exec_(QtGui.QCursor.pos())
def add_item_dialog(self, title):
text, ok = QtGui.QInputDialog.getText(
self,
"Add {0} Item".format(title),
"Enter name for {0}-Item:".format(title)
)
if ok and text != "":
return text
def add_new_parent_item(self):
input_text = self.add_item_dialog("Parent")
if input_text:
CustomTreeWidgetItem(self, input_text, is_tristate=True)
def adds_new_child_item(self, base_node):
input_text = self.add_item_dialog("Sub")
if input_text:
CustomTreeWidgetItem(base_node, input_text)
self.setItemExpanded(base_node, True)
def remove_selected_item(self):
root = self.invisibleRootItem()
for item in self.selectedItems():
(item.parent() or root).removeChild(item)
def derive_tree_items(self, mode="all"):
all_items = defaultdict(list)
root_item = self.invisibleRootItem()
top_level_count = root_item.childCount()
for i in range(top_level_count):
top_level_item = root_item.child(i)
top_level_item_name = str(top_level_item.text(0))
child_num = top_level_item.childCount()
all_items[top_level_item_name] = []
for n in range(child_num):
child_item = top_level_item.child(n)
child_item_name = str(child_item.text(0)) or ""
if mode == "all":
all_items[top_level_item_name].append(child_item_name)
elif mode == "checked":
if child_item.checkState(0) == QtCore.Qt.Checked:
all_items[top_level_item_name].append(child_item_name)
elif mode == "unchecked":
if child_item.checkState(0) == QtCore.Qt.Unchecked:
all_items[top_level_item_name].append(child_item_name)
return all_items
class MainApp(QtGui.QWidget):
def __init__(self):
super(MainApp, self).__init__()
# initial dictionary that is populated into the tree widget
test_dict = {
"menuA": ["a101", "a102"],
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"],
}
self._tree = CustomTreeWidget()
self._tree.header().hide()
for pk, pv in sorted(test_dict.items()):
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
self.orig_dict = self._tree.derive_tree_items()
# Expand the hierarchy by default
self._tree.expandAll()
tree_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("Add new item")
self.btn2 = QtGui.QPushButton("Highlight Diff.")
tree_layout.addWidget(self._tree)
tree_layout.addWidget(self.btn1)
tree_layout.addWidget(self.btn2)
main_layout = QtGui.QHBoxLayout()
main_layout.addLayout(tree_layout)
self.setLayout(main_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.add_parent_item)
self.btn2.clicked.connect(self.highlight_diff)
def add_parent_item(self):
# Get current selected in list widget
# CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
self._tree.add_new_parent_item()
def highlight_diff(self):
self.current_dict = self._tree.derive_tree_items()
if self.orig_dict != self.current_dict:
# check for key difference
diff = [k for k in self.current_dict if k not in self.orig_dict]
if diff:
# get the difference of top-level items
for d in diff:
top_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
#print aaa[0].childCount()
top_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))
if top_item[0].childCount():
for n in range(top_item[0].childCount()):
top_item[0].child(n).setTextColor(0, QtGui.QColor(255, 0, 0))
# to highlight the child diff. of existing top-items
# issue with this portion if the new added item name already existed
for k, v in self.current_dict.items():
if k in self.orig_dict:
diff = set(v).difference(self.orig_dict.get(k), [])
for d in diff:
child_item = self._tree.findItems(d, QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive)
child_item[0].setTextColor(0, QtGui.QColor(255, 0, 0))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
You can save in a role a flag indicating if it is a new item or not and change the color using a delegate:
import sys
from functools import partial
from PyQt4 import QtCore, QtGui
from collections import defaultdict
IsNewItemRole = QtCore.Qt.UserRole + 1000
class CustomTreeDelegate(QtGui.QStyledItemDelegate):
#property
def text_color(self):
if not hasattr(self, "_text_color"):
self._text_color = QtGui.QColor()
return self._text_color
#text_color.setter
def text_color(self, color):
self._text_color = color
def initStyleOption(self, option, index):
super(CustomTreeDelegate, self).initStyleOption(option, index)
if self.text_color.isValid() and index.data(IsNewItemRole):
option.palette.setBrush(QtGui.QPalette.Text, self.text_color)
class CustomTreeWidgetItem(QtGui.QTreeWidgetItem):
def __init__(self, parent=None, text="", is_tristate=False, is_new_item=False):
super(CustomTreeWidgetItem, self).__init__(parent)
self.setText(0, text)
flags = QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
if is_tristate:
flags |= QtCore.Qt.ItemIsTristate
else:
self.setCheckState(0, QtCore.Qt.Unchecked)
self.setFlags(self.flags() | flags)
self.setData(0, IsNewItemRole, is_new_item)
def setData(self, column, role, value):
state = self.checkState(column)
QtGui.QTreeWidgetItem.setData(self, column, role, value)
if role == QtCore.Qt.CheckStateRole and state != self.checkState(column):
tree_widget = self.treeWidget()
if isinstance(tree_widget, CustomTreeWidget):
tree_widget.itemToggled.emit(self, column)
class CustomTreeWidget(QtGui.QTreeWidget):
itemToggled = QtCore.pyqtSignal(QtGui.QTreeWidgetItem, int)
def __init__(self, widget=None):
super(CustomTreeWidget, self).__init__(widget)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_custom_menu)
def show_custom_menu(self, pos):
base_node = self.itemAt(pos)
if base_node is None:
return
qmenu = QtGui.QMenu(self)
remove_action = QtGui.QAction("Remove item", self)
remove_action.triggered.connect(self.remove_selected_item)
qmenu.addAction(remove_action)
# The following options are only effected for top-level items
# top-level items do not have `parent()`
if base_node.parent() is None:
add_new_child_action = QtGui.QAction("Add new sub item", self)
add_new_child_action.triggered.connect(
partial(self.adds_new_child_item, base_node)
)
# qmenu.addAction(add_new_child_action)
qmenu.insertAction(remove_action, add_new_child_action)
qmenu.exec_(self.mapToGlobal(pos))
def add_item_dialog(self, title):
text, ok = QtGui.QInputDialog.getText(
self, "Add {0} Item".format(title), "Enter name for {0}-Item:".format(title)
)
if ok:
return text
def add_new_parent_item(self):
input_text = self.add_item_dialog("Parent")
if input_text:
it = CustomTreeWidgetItem(
self, input_text, is_tristate=True, is_new_item=True
)
def adds_new_child_item(self, base_node):
input_text = self.add_item_dialog("Sub")
if input_text:
it = CustomTreeWidgetItem(base_node, input_text, is_new_item=True)
self.setItemExpanded(base_node, True)
it.setData(0, IsNewItemRole, True)
def remove_selected_item(self):
root = self.invisibleRootItem()
for item in self.selectedItems():
(item.parent() or root).removeChild(item)
class MainApp(QtGui.QWidget):
def __init__(self, parent=None):
super(MainApp, self).__init__(parent)
# initial dictionary that is populated into the tree widget
test_dict = {
"menuA": ["a101", "a102"],
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"],
}
self._tree = CustomTreeWidget()
self._tree.header().hide()
self._tree_delegate = CustomTreeDelegate(self._tree)
self._tree.setItemDelegate(self._tree_delegate)
for pk, pv in sorted(test_dict.items()):
parent = CustomTreeWidgetItem(self._tree, pk, is_tristate=True)
for c in pv:
child = CustomTreeWidgetItem(parent, c)
# Expand the hierarchy by default
self._tree.expandAll()
tree_layout = QtGui.QVBoxLayout()
self.btn1 = QtGui.QPushButton("Add new item")
self.btn2 = QtGui.QPushButton("Highlight Diff.")
tree_layout.addWidget(self._tree)
tree_layout.addWidget(self.btn1)
tree_layout.addWidget(self.btn2)
main_layout = QtGui.QHBoxLayout(self)
main_layout.addLayout(tree_layout)
self.setup_connections()
def setup_connections(self):
self.btn1.clicked.connect(self.add_parent_item)
self.btn2.clicked.connect(self.highlight_diff)
def add_parent_item(self):
# Get current selected in list widget
# CustomTreeWidgetItem(self._tree, "test", is_tristate=True)
self._tree.add_new_parent_item()
def highlight_diff(self):
self._tree_delegate.text_color = QtGui.QColor(255, 0, 0)
self._tree.viewport().update()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
w = MainApp()
w.show()
sys.exit(app.exec_())
I am creating a custom qtreewidget class that autoresizes its window to fit exactly the currently visible elements (I don't want to have to scroll). To do this, I run a count function to find the number of open qtreewidgetitems and their children and set a fixed height from there. However, when I expand a child widget (click the expand arrow on one of my items) the whole view suddenly needs to scroll because there is extra white space at the bottom, despite my count function accurately calculating the needed height. How do I get rid of it?
Below is a working class that can be run directly as is.
import sys
from PyQt4 import QtGui
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class TreeWidget(QTreeWidget):
def __init__(self, parent=None):
super(TreeWidget, self).__init__()
self.installEventFilter(self)
self.setStyleSheet('''
background: None;
border: None;
outline: None;
outline-width: 0px;
selection-background-color: blue;
''')
header = QtGui.QTreeWidgetItem(["Tree", "First"])
self.setAutoScroll(False)
self.setHeaderItem(header)
self.header().close()
# self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def fill_item(self, item, value):
item.setExpanded(False)
if type(value) is dict:
for key, val in sorted(value.iteritems()):
child = QTreeWidgetItem()
child.setText(0, unicode(key))
# child.doubleClicked.connect(lambda: self.doubleClicked1(child))
item.addChild(child)
self.fill_item(child, val)
elif type(value) is list:
last = None
for val in value:
child = QTreeWidgetItem()
if type(val) is dict:
item.addChild(child)
child.setText(0, '[dict]')
# child.doubleClicked.connect(lambda: self.doubleClicked1(child))
self.fill_item(child, val)
last = child
elif type(val) is list:
self.fill_item(last, val)
else:
item.addChild(child)
child.setText(0, unicode(val))
child.setText(1, 'test')
# child.doubleClicked.connect(lambda: self.doubleClicked1(child))
child.setExpanded(False)
last = child
else:
child = QTreeWidgetItem()
child.setText(0, unicode(val))
child.setText(1, 'test')
# child.doubleClicked.connect(lambda: self.doubleClicked1(child))
item.addChild(child)
def fill_widget(self, value):
self.clear()
self.fill_item(self.invisibleRootItem(), value)
def resizeEvent(self, event):
self.resize()
def resize(self):
width = 50
self.header().resizeSection(1, width)
self.header().resizeSection(0, self.width()-width)
height = self.visibleCount()
print height/15
self.setFixedHeight(height+0)
def eventFilter(self, source, event):
if source is self:
if event.type() == 1:
self.resize()
elif event.type() == QEvent.Leave:
self.clearSelection()
return QtGui.QTreeWidget.eventFilter(self, source, event)
def visibleCount(self, parent=0):
height = 0
if parent == 0:
topHeight = 0
for a in xrange(self.topLevelItemCount()):
item = self.topLevelItem(a)
topHeight += self.visualItemRect(item).height()
if item.isExpanded():
height += self.visibleCount(item)
height += topHeight
else:
childHeight = 0
for a in xrange(parent.childCount()):
item = parent.child(a)
childHeight += self.visualItemRect(item).height()
if item.isExpanded():
height += self.visibleCount(item)
height += childHeight
return height
def editClicked(self, parent=0):
# print 'edit 2'
if parent == 0:
for a in xrange(self.topLevelItemCount()):
item = self.topLevelItem(a)
print type(item)
item.setExpanded(True)
self.editClicked(item)
else:
for a in xrange(parent.childCount()):
item = parent.child(a)
print type(item)
item.setText(1, '+')
item.setExpanded(True)
self.editClicked(item)
def doubleClicked1(self, widget):
print widget
def main():
app = QtGui.QApplication(sys.argv)
ex = TreeWidget()
data = [
'Make sure ZMQ remote button gets honored',
'Fill in plot',
'Remove cycle',
'Remove current control or make it working',
'Handle possible startup errors with dialogs',
'Get improved current read-out (requires hardware changes)',
['1','2','3'],
'Email quench notification'
]
ex.fill_widget(data)
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You can remove the event filter entirely. You need to resize the widget when the items are expanded or collapsed. Both actions trigger signals, so you can simply connect those signals to resize:
class TreeWidget(QTreeWidget):
def __init__(self, parent=None):
super(TreeWidget, self).__init__(parent)
self.expanded.connect(self.resize)
self.collapsed.connect(self.resize)
I can add a single tooltip to all headers using
tableview = QTableView()
tableview.horizontalHeader().setToolTip("headers")
but can I add different tooltips to each header, i.e. I need to access the QWidgets that contains the headers, e.g. (not working):
tableview.horizontalHeader().Item[0].setToolTip("header 0")
I'm pretty new to this stuff too, but I think you'll need to subclass QTableView and reimplement the headerData function. Here is a working example. Hopefully you can extract what you need from it:
from PyQt4 import QtGui, QtCore
import sys
class PaletteListModel(QtCore.QAbstractListModel):
def __init__(self, colors = [], parent = None):
QtCore.QAbstractListModel.__init__(self,parent)
self.__colors = colors
# required method for Model class
def rowCount(self, parent):
return len(self.__colors)
# optional method for Model class
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QString("Palette")
else:
return QtCore.QString("Color %1").arg(section)
if role == QtCore.Qt.ToolTipRole:
if orientation == QtCore.Qt.Horizontal:
return QtCore.QString("Horizontal Header %s Tooltip" % str(section))
else:
return QtCore.QString("Vertical Header %s Tooltip" % str(section))
# required method for Model class
def data(self, index, role):
# index contains a QIndexClass object. The object has the following
# methods: row(), column(), parent()
row = index.row()
value = self.__colors[row]
# keep the existing value in the edit box
if role == QtCore.Qt.EditRole:
return self.__colors[row].name()
# add a tooltip
if role == QtCore.Qt.ToolTipRole:
return "Hex code: " + value.name()
if role == QtCore.Qt.DecorationRole:
pixmap = QtGui.QPixmap(26,26)
pixmap.fill(value)
icon = QtGui.QIcon(pixmap)
return icon
if role == QtCore.Qt.DisplayRole:
return value.name()
def setData(self, index, value, role = QtCore.Qt.EditRole):
row = index.row()
if role == QtCore.Qt.EditRole:
color = QtGui.QColor(value)
if color.isValid():
self.__colors[row] = color
self.dataChanged.emit(index, index)
return True
return False
# implment flags() method
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
app.setStyle("plastique")
data = QtCore.QStringList()
data << "one" << "two" << "three" << "four" << "five"
tableView = QtGui.QTableView()
tableView.show()
red = QtGui.QColor(255,0,0)
green = QtGui.QColor(0,255,0)
blue = QtGui.QColor(0,0,255)
model = PaletteListModel([red, green, blue])
tableView.setModel(model)
sys.exit(app.exec_())
Here's what worked for me:
headerView = self._table.horizontalHeader()
for i in range(headerView.count()):
key = headerView.model().headerData(i, QtCore.Qt.Horizontal)
toolTip = myDictOfToolTips.get(key, None)
self._table.horizontalHeaderItem(i).setToolTip(toolTip)
QTableWidget (which inherits QTableView) has a method horizontalHeaderItem(int) which can be used to get the header items, so you maybe could swich to use that instead of QTableView?
If you use QTableView, you can set tooltip by QStandardItemModel:
QStandardItemModel myModel;
myModel.horizontalHeaderItem(1)->setToolTip("");