I have a popup that only contains a QTextEdit, it has a lot of text in it, a lot of lines. I want it to scroll to a certain line in the QTextEdit on show(). So that the line I want is at the top.
Code snippet:
editor = QtGui.QTextEdit()
# fill the editor with text
# set the scroll to nth line
editor.show()
How can I achieve that?
Update
I've managed to get it to show the nth line at the bottom:
cursor = QtGui.QTextCursor(editor.document().findBlockByLineNumber(n))
editor.moveCursor(QtGui.QTextCursor.End)
editor.setTextCursor(cursor)
For example for n=25 I get:
_______________________
.
.
.
.
25th line
_______________________
But I need it to be at the top...
You almost have it. The trick is to move the current cursor to the bottom first, and then reset the cursor to the target line. The view will then automatically scroll to make the cursor visible:
editor.moveCursor(QtGui.QTextCursor.End)
cursor = QtGui.QTextCursor(editor.document().findBlockByLineNumber(n))
editor.setTextCursor(cursor)
By extension, to position the cursor at the bottom, move the current cursor to the start first:
editor.moveCursor(QtGui.QTextCursor.Start)
...
Here's a demo script:
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.edit = QtGui.QTextEdit(self)
self.edit.setPlainText(
'\n'.join('%04d - blah blah blah' % i for i in range(200)))
self.button = QtGui.QPushButton('Go To Line', self)
self.button.clicked.connect(self.handleButton)
self.spin = QtGui.QSpinBox(self)
self.spin.setRange(0, 199)
self.spin.setValue(50)
self.check = QtGui.QCheckBox('Scroll Top')
self.check.setChecked(True)
layout = QtGui.QGridLayout(self)
layout.addWidget(self.edit, 0, 0, 1, 3)
layout.addWidget(self.button, 1, 0)
layout.addWidget(self.spin, 1, 1)
layout.addWidget(self.check, 1, 2)
QtCore.QTimer.singleShot(0, lambda: self.scrollToLine(50))
def scrollToLine(self, line=0):
if self.check.isChecked():
self.edit.moveCursor(QtGui.QTextCursor.End)
else:
self.edit.moveCursor(QtGui.QTextCursor.Start)
cursor = QtGui.QTextCursor(
self.edit.document().findBlockByLineNumber(line))
self.edit.setTextCursor(cursor)
def handleButton(self):
self.scrollToLine(self.spin.value())
self.edit.setFocus()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 100, 400, 300)
window.show()
sys.exit(app.exec_())
Related
I have a QTreeView widget in which I have set indentation to 0 with QTreeView::setIndentation(0), but when editing an item, the editor still appears at the indentation level of the default indented behaviour:
I have tried changing the editor position with a QStyledItemDelegate::updateEditorGeometry's method, but it seems that there is a limit to where I can move the editor horizontally, as I cannot move it to a negative offset, past the x == 0 position, like for example QRect(-50, 0, 100, 30).
Any ideas would be greatly appreciated, thanks.
P.S.:
Here is an attempt of a minimal-reproducible-example, but this has another bug: the editor widget does not display when the geometry of the widget in drawRow is changed! The editor works when typing and pressing enter, it's just not displayed! I'll try to figure out what's the difference between my example in the image above and this code.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sip
class MyTreeWidget(QTreeWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setIndentation(0)
self.setMouseTracking(True)
self.setUniformRowHeights(True)
self.setExpandsOnDoubleClick(False)
self.setColumnCount(1)
self.setHeaderLabels(["Items"])
self.setHeaderHidden(True)
self.header().setVisible(False)
self.header().setStretchLastSection(False)
self.header().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
self.add_items()
self.itemClicked.connect(self.click)
def click(self, item, column):
item.setFlags(
Qt.ItemFlag.ItemIsEditable |
Qt.ItemFlag.ItemIsEnabled |
Qt.ItemFlag.ItemIsSelectable
)
index = self.indexFromItem(item, 0)
self.edit(index)
def drawRow(self, painter, option, index):
model = self.model()
row = index.row()
column = index.column()
item = self.itemFromIndex(index)
widget = self.itemWidget(item, 0)
if widget is not None:
geo = QRect(widget.geometry())
geo.setX(geo.x() + 50)
widget.setGeometry(geo) # <- This line causes the editor's horizontal offset
super().drawRow(painter, option, index)
def add_items(self):
items = [
'Cookie dough',
'Hummus',
'Spaghetti',
'Dal makhani',
'Chocolate whipped cream'
]
parent = None
for item in items:
new_item = QTreeWidgetItem(None)
if parent is not None:
new_item = QTreeWidgetItem(parent)
else:
self.addTopLevelItem(new_item)
new_item.setText(0, item)
new_item.setExpanded(True)
parent = new_item
def edit_last_item(*args):
new_item.setFlags(
Qt.ItemFlag.ItemIsEditable |
Qt.ItemFlag.ItemIsEnabled |
Qt.ItemFlag.ItemIsSelectable
)
index = self.indexFromItem(new_item, 0)
self.edit(index)
print("Editing")
QTimer.singleShot(1000, edit_last_item)
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.setLayout(layout)
layout.addWidget(MyTreeWidget())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
In trying to create a minimal reproducible example as #Parisa.H.R mentioned in the comment, I discovered that in the drawRow method in the QTreeView/QTreeWidget there is some code that adjusts the geometry of item widgets, like so (Python/PyQt example):
class MyTreeView(QTreeView):
def drawRow(self, painter, option, index):
model = self.model()
row = index.row()
column = index.column()
item = self.itemFromIndex(index)
widget = self.itemWidget(item, 0)
geo = ... # <- Code that calculates new geometry
widget.setGeometry(geo) # <- This line causes the editor's horizontal offset
super().drawRow(painter, option, index)
which is what causes the editor widget to be moved horizontally to the right.
Thanks #Parisa.H.R
Currently, QTextEdit permits selecting text and then altering that selection with shift-click-drag only on the side of the selection opposite the anchor. The anchor is placed where the selection started. If the user tries to alter the selection near the start, the selection pivots around the anchor point instead of extending. I'd like to permit changing the selection from either side.
My first attempt is to simply set the anchor on the opposite side from where the cursor is located. Say, for example, the selection is from 10 to 20. If the cursor is shift-click-dragged at position 8, then the anchor would be set to 20. If the cursor is shift-click-dragged at position 22, then the anchor would be set to 10. Later, I'll try something more robust, perhaps based on the center point of the selection.
I thought this code would work, but it does not seem to affect the default behavior at all. What have I missed?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setMouseTracking(True)
def mouseMoveEvent(self, event):
point = QPoint()
x = event.x() #these are relative to the upper left corner of the text edit window
y = event.y()
point.setX(x)
point.setY(y)
self.mousepos = self.cursorForPosition(point).position() # get character position of current mouse using local window coordinates
if event.buttons()==Qt.LeftButton:
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ShiftModifier:
start = -1 #initialize to something impossible
end = -1
cursor = self.textCursor()
select_point1 = cursor.selectionStart()
select_point2 = cursor.selectionEnd()
if select_point1 < select_point2: # determine order of selection points
start = select_point1
end = select_point2
elif select_point2 < select_point1:
start = select_point2
end = select_point1
if self.mousepos > end: # if past end when shift-click then trying to extend right
cursor.setPosition(start, mode=QTextCursor.MoveAnchor)
elif self.mousepos < start: # if before start when shift-click then trying to extend left
cursor.setPosition(end, mode=QTextCursor.MoveAnchor)
if start != -1 and end != -1: #if selection exists then this should trigger
self.setTextCursor(cursor)
super().mouseMoveEvent(event)
Here's a first stab at implementing shift+click extension of the current selection. It seems to work okay, but I have not tested it to death, so there may be one or two glitches. The intended behaviour is that a shift+click above or below the selection should extend the whole selection in that direction; and a shift+click with drag should do the same thing, only continuously.
Note that I have also set the text-interaction flags so that the caret is visible in read-only mode, and the selection can also be manipulated with the keyboard in various ways (e.g. ctrl+shift+right extends the selection to the next word).
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class TextEditor(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
self.setTextInteractionFlags(
Qt.TextSelectableByMouse |
Qt.TextSelectableByKeyboard)
def mouseMoveEvent(self, event):
if not self.setShiftSelection(event, True):
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
if not self.setShiftSelection(event):
super().mousePressEvent(event)
def setShiftSelection(self, event, moving=False):
if (event.buttons() == Qt.LeftButton and
QApplication.keyboardModifiers() == Qt.ShiftModifier):
cursor = self.textCursor()
start = cursor.selectionStart()
end = cursor.selectionEnd()
if not moving or start != end:
anchor = cursor.anchor()
pos = self.cursorForPosition(event.pos()).position()
if pos <= start:
start = pos
elif pos >= end:
end = pos
elif anchor == start:
end = pos
else:
start = pos
if pos <= anchor:
start, end = end, start
cursor.setPosition(start, QTextCursor.MoveAnchor)
cursor.setPosition(end, QTextCursor.KeepAnchor)
self.setTextCursor(cursor)
return True
return False
if __name__ == '__main__':
app = QApplication(sys.argv)
window = TextEditor()
window.setText(open(__file__).read())
window.setGeometry(600, 50, 800, 800)
window.show()
sys.exit(app.exec_())
I am working on a simple app for a tea timer with Python3 and PyQt5 on OSX. For aesthetic reasons I decided to remove the window frame. In order to still keep the window moveable I overloaded the mousePressEvent() and mouseMoveEvent() handlers.
However I run into a problem when the window is being moved while the mouse is over one of the buttons (QPushButton()). The cursor then jumps to the last click position and the window is moved from there. Does anyone have an idea as to why this is happening? Interestingly the same issue does not appear when clicking and moving on a QLabel() object...
If you want to try it out, here is an example code:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
## ===============================================================
## IMPORTS
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
## ===============================================================
## CONSTANTS
WINDOW_WIDTH = 690
WINDOW_HEIGHT = 435
## ===============================================================
## CLASSES
class Form(QWidget):
def __init__(self, parent=None):
super().__init__()
self.windowPos = QPoint() # Maybe not necessary when button click and window movement are seperated
# Declare and specify UI elements
self.timerLabel = QLabel("00:00") # Might have to change data type here
self.timerLabel.setObjectName("timerLabel")
self.infoLabel = QLabel("No tea selected")
self.infoLabel.setObjectName("infoLabel")
self.teaOneButton = QPushButton("Tea One")
self.teaOneButton.setObjectName("teaOneButton")
self.teaTwoButton = QPushButton("Tea Two")
self.teaTwoButton.setObjectName("teaTwoButton")
# Arrange UI elements in a layout
grid = QGridLayout()
self.setLayout(grid) # Set the QGridLayout as the window's main layout
grid.setSpacing(0) # Spacing between widgets - does not work if window is resized
grid.setContentsMargins(4, 4, 4, 4)
grid.addWidget(self.timerLabel, 0, 0, 1, -1, Qt.AlignHCenter) # http://doc.qt.io/qt-5/qgridlayout.html#addWidget
grid.addWidget(self.infoLabel, 1, 0, 1, -1, Qt.AlignHCenter)
grid.addWidget(self.teaOneButton, 2, 0)
grid.addWidget(self.teaTwoButton, 2, 1)
self.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
# Arranging window in center of the screen by overloading showEvent method
def showEvent(self, QShowEvent):
self.centerOnScreen()
def centerOnScreen(self):
screen = QDesktopWidget()
screenGeom = QRect(screen.screenGeometry(self))
screenCenterX = screenGeom.center().x()
screenCenterY = screenGeom.center().y()
self.move(screenCenterX - self.width() / 2,
screenCenterY - self.height() / 2)
# Overload mouseEvent handlers to make window moveable
def mousePressEvent(self, QMouseEvent):
self.windowPos = QMouseEvent.pos()
self.setCursor(QCursor(Qt.SizeAllCursor))
def mouseReleaseEvent(self, QMouseEvent):
self.setCursor(QCursor(Qt.ArrowCursor))
def mouseMoveEvent(self, QMouseEvent):
# print (self.childAt(QMouseEvent.pos()) == QLabel)
pos = QPoint(QMouseEvent.globalPos())
self.window().move(pos - self.windowPos)
## ===============================================================
## MAIN LOOP
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
screen = Form()
screen.setWindowFlags(Qt.FramelessWindowHint)
screen.show()
sys.exit(app.exec_())
Well guys I think I might have found a way to circumvent the problem.
It seemed to me, that the problem might be that QPushButtons handle mouseMoveEvents differently than, for example QLabel objects. To me it makes sense intuitively since a button object might need different mouse event handling than other unclickable objects.
So what I did was try to implement a subclass for QPushButton, overloading the mouseMoveEvent handler. At first I thought I might set the handler to ignore the event in order to delegate it up to the parent widget. That did not change anything. It actually might be something that has been going on all along. Because when I implement some sort of code into the event handling function, the button does no longer function as a source for moving the window. See for yourself:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
## ===============================================================
## IMPORTS
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
## ===============================================================
## CONSTANTS
WINDOW_WIDTH = 690
WINDOW_HEIGHT = 435
## ===============================================================
## CLASSES
class UnmoveableButton(QPushButton):
def __init__(self, text=""):
super().__init__(text)
# This event function is overloaded in order to avoid the widget from delegating the event up to the parent.
# This way, the pre-existing functionality is skipped, i.e. the window can no longer be moved while hovering over a button.
def mouseMoveEvent(self, QMouseEvent):
pass
class Form(QWidget):
def __init__(self, parent=None):
super().__init__()
self.windowPos = QPoint() # Maybe not necessary when button click and window movement are seperated
# Declare and specify UI elements
self.timerLabel = QLabel("00:00") # Might have to change data type here
self.timerLabel.setObjectName("timerLabel")
self.infoLabel = QLabel("No tea selected")
self.infoLabel.setObjectName("infoLabel")
self.teaOneButton = UnmoveableButton("Tea One")
self.teaOneButton.setObjectName("teaOneButton")
self.teaTwoButton = UnmoveableButton("Tea Two")
self.teaTwoButton.setObjectName("teaTwoButton")
# Arrange UI elements in a layout
grid = QGridLayout()
self.setLayout(grid) # Set the QGridLayout as the window's main layout
grid.setSpacing(0) # Spacing between widgets - does not work if window is resized
grid.setContentsMargins(4, 4, 4, 4)
grid.addWidget(self.timerLabel, 0, 0, 1, -1, Qt.AlignHCenter) # http://doc.qt.io/qt-5/qgridlayout.html#addWidget
grid.addWidget(self.infoLabel, 1, 0, 1, -1, Qt.AlignHCenter)
grid.addWidget(self.teaOneButton, 2, 0)
grid.addWidget(self.teaTwoButton, 2, 1)
self.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
# Arranging window in center of the screen by overloading showEvent method
def showEvent(self, QShowEvent):
self.centerOnScreen()
def centerOnScreen(self):
screen = QDesktopWidget()
screenGeom = QRect(screen.screenGeometry(self))
screenCenterX = screenGeom.center().x()
screenCenterY = screenGeom.center().y()
self.move(screenCenterX - self.width() / 2,
screenCenterY - self.height() / 2)
# Overload mouseEvent handlers to make window moveable
def mousePressEvent(self, QMouseEvent):
self.windowPos = QMouseEvent.pos()
self.setCursor(QCursor(Qt.SizeAllCursor))
def mouseReleaseEvent(self, QMouseEvent):
self.setCursor(QCursor(Qt.ArrowCursor))
def mouseMoveEvent(self, QMouseEvent):
# print (self.childAt(QMouseEvent.pos()) == QLabel)
pos = QPoint(QMouseEvent.globalPos())
self.window().move(pos - self.windowPos)
## ===============================================================
## MAIN LOOP
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
screen = Form()
screen.setWindowFlags(Qt.FramelessWindowHint)
screen.show()
sys.exit(app.exec_())
I think it might not be the most elegant or correct solution for that matter, but it works fine for me right now. If someone has an idea on how to improve upon this, let me know :)
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
Are you sure there is no namespace clash from imported members?
I use wx.grid.Grid for display table.
The number of grid rows is changed and it may be more than 2000.
For number of rows less than 1723 GUI displays normally.
But if I have greater than or equal to 1723 rows, GUI displays incorrectly:
Sudden additional vertical scroll bar appears.
If aim a mouse cursor on this additional scrollbar then my laptop display blinks
wx.grid.Grid don't expands into ScrolledPanel.
How can I use wx.grid.Grid with greate number of rows (more than 2000)?
Thanks in advance.
Code Sample:
import wx
import wx.grid
from wx.lib.scrolledpanel import ScrolledPanel
class TestPanel(ScrolledPanel):
def __init__(self, parent):
ScrolledPanel.__init__(self, parent, wx.ID_ANY, size=(640, 480))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self._create_table(), 1, wx.EXPAND | wx.ALL, 5)
self.SetSizer(self.sizer)
self.SetupScrolling()
self.SetAutoLayout(1)
def _create_table(self):
_table = wx.grid.Grid(self, -1)
_table.CreateGrid(0, 1)
for i in xrange(1723): # Work normally If I use 1722 rows
_table.AppendRows()
_table.SetCellValue(i, 0, str(i))
return _table
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY,
title="Scroll table", size=(640, 480))
self.fSizer = wx.BoxSizer(wx.VERTICAL)
self.fSizer.Add(TestPanel(self), 1, wx.EXPAND)
self.SetSizer(self.fSizer)
self.Show()
if __name__ == "__main__":
app = wx.App(False)
frame = TestFrame()
app.MainLoop()
In this case you should use a wx.Panel instead of the ScrolledPanel. The grid is able to manage its scrolling on its own, it doesn't need to have any help from its parent.
If you have other widgets to display that wont fit in the panel, and you want to be able to scroll them and the grid in and out of view, then using the ScrolledPanel would be appropriate, but then you should do something to constrain the size of the grid so it doesn't try to expand to show all cells.
I am converting a larger program of QGraphicsItems to QGraphicsWidgets (let's call them item and widget for typing sake). Mouse hover fails now because the position and/or rect of the widgets are not the same as the old items. I've boiled down to a simple case with a view, scene, an item and a widget. The blue item renders at 100x50 pix, and hoverEnterEvent occurs as expected. However, the red widget is rendered at half intended width. I can fix this if I reimplement the pure virtual function boundingRect for the widget but the hover event is still only triggered atop the 50x50 left half. What pos/rect/geometry methods do I need to use/override to get the widget to interact properly with the mouse just like the item? Thanks. Here's my sample code
#!/usr/local/bin/python
import os, sys
from PyQt4.Qt import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyView(QGraphicsView):
def __init__(self):
QGraphicsView.__init__(self)
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.scene = QGraphicsScene(self)
self.item = GraphicsItem('item', 100, 50)
self.item.moveBy(50, 50)
self.scene.addItem(self.item)
self.widget = GraphicsWidget('widget', 100, 50)
self.scene.addItem(self.widget)
self.setScene(self.scene)
class GraphicsItem(QGraphicsItem):
def __init__(self, name, width, height):
QGraphicsItem.__init__(self)
self.setAcceptHoverEvents(True)
self.name = name
self.__width = width
self.__height = height
def boundingRect(self):
return QRectF(0, 0, self.__width, self.__height)
def hoverEnterEvent(self, event):
self.__printGeometryDetails()
def paint(self, painter, option, widget):
bgRect = self.boundingRect()
painter.drawRects(bgRect)
painter.fillRect(bgRect, QColor('blue'))
def __printGeometryDetails(self):
print self.name
print ' pos (%.0f, %0.0f)' % (self.pos().x(), self.pos().y())
print ' boundingRect (%.0f, %0.0f, %.0f, %0.0f)' % (self.boundingRect().x(), self.boundingRect().y(), self.boundingRect().width(), self.boundingRect().height())
class GraphicsWidget(QGraphicsWidget):
def __init__(self, name, width, height):
QGraphicsWidget.__init__(self)
self.setAcceptHoverEvents(True)
self.name = name
self.__width = width
self.__height = height
def boundingRect(self):
return QRectF(0, 0, self.__width, self.__height)
def hoverEnterEvent(self, event):
self.__printGeometryDetails()
def paint(self, painter, option, widget):
bgRect = self.boundingRect()
painter.drawRects(bgRect)
painter.fillRect(bgRect, QColor('red'))
def __printGeometryDetails(self):
print self.name
print ' pos (%.0f, %0.0f)' % (self.pos().x(), self.pos().y())
print ' boundingRect (%.0f, %0.0f, %.0f, %0.0f)' % (self.boundingRect().x(), self.boundingRect().y(), self.boundingRect().width(), self.boundingRect().height())
print ' geometry (%.0f, %0.0f, %.0f, %0.0f)' % (self.geometry().x(), self.geometry().y(), self.geometry().width(), self.geometry().height())
print ' rect (%.0f, %0.0f, %.0f, %0.0f)' % (self.rect().x(), self.rect().y(), self.rect().width(), self.rect().height())
if __name__ == '__main__':
app = QApplication(sys.argv)
view = MyView()
view.setGeometry(600, 100, 400, 370)
view.show()
sys.exit(app.exec_())
It does seem to work correctly if you use self.resize(width, height) instead of redefining boundingRect.