PyGTK: Make a TextView that behaves like OpenOffice's - python

I am trying to make an "Office-like" TextView. That is:
The TextView itself has a fixed width (so it kinda shows what the text would look like on a sheet of paper)
If the window (on which the TextView is packed) is smaller than the fixed width: The TextView should be scrollable
If the window is bigger, add margins to the left/right to keep the fixed width
This is what i came up with, and it actually behaves like it should, except that it doesn't scroll if your cursor gets out of the viewport, when you for example write a line that needs more space than the windows current width.
What would be the best way to keep the viewport "in sync"? Do I have to create a custom Viewport?
Thanks in advance!
#!/usr/bin/env python2
# encoding: utf-8
import gtk
class SheetTextView(gtk.TextView):
WIDTH = 700
def __init__(self):
gtk.TextView.__init__(self)
self.set_wrap_mode(gtk.WRAP_WORD)
self.set_size_request(self.WIDTH, -1)
self.connect('size-allocate', self._on_size_allocate)
def _on_size_allocate(self, widget, event, data=None):
# Reset left/right margin to simulate a fixed line width
x, y, width, height = self.get_allocation()
if width > self.WIDTH:
margin = (width - self.WIDTH) / 2
self.set_left_margin(margin)
self.set_right_margin(margin)
if __name__ == "__main__":
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.connect('delete_event', gtk.main_quit)
view = SheetTextView()
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scroll.add_with_viewport(view)
window.add(scroll)
window.show_all()
gtk.main()

You mean something like this?
Dropbox link
Please note this is just a test, all it does for now is change the size when necessary,
is that what you meant?
If so, please tell me and I'll fix the bugs and improve.
EDIT: Please note this is just scratch code, messy coded...

Related

How to make QTableWidget fill all available window space and show vertical scroll bar if it's larger, but avoid blank white space if it's smaller?

I want my QTableWidget to:
Fill all available space inside the window
Show vertical scroll bar if it's longer than the provided space
Adapt to changing window geometry
Do not show blank white space underneath, if it's smaller than the provided space (without stretching bottom row).
So basically what I want to see:
Short table without white space
Long table fills no less no more than available and shows scroll bar
But I can't achieve these two states simultaneously.
What I get instead:
Ugly white space beneath the table
Table stretches the window itself
The difference in the two behaviors is in calculating table height manually. If I do so, a long table stretches the window, but otherwise it tries to fill all unused space with blank white.
Here is my minimal code:
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
import os
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 200, 100)
# self.tableLabel = QLabel() # I thought it might help somehow
self.table = QTableWidget(4, 1)
self.setTable(4)
self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.fewRowsBtn = QPushButton('Few rows')
self.manyRowsBtn = QPushButton('Many rows')
self.button = QPushButton('Button')
self.button.setFixedWidth(120)
self.fewRowsBtn.clicked.connect(lambda: self.setTable(4))
self.manyRowsBtn.clicked.connect(lambda: self.setTable(20))
self.map = QLabel()
canvas = QPixmap(320, 320)
canvas.fill()
self.map.setPixmap(canvas)
leftLayout = QVBoxLayout()
leftLayout.addWidget(self.fewRowsBtn)
leftLayout.addWidget(self.manyRowsBtn)
leftLayout.addWidget(self.map)
rightLayout = QVBoxLayout()
rightLayout.addWidget(self.table)
rightLayout.addWidget(self.button) #, alignment=Qt.AlignmentFlag.AlignBottom)
generalLayout = QHBoxLayout()
generalLayout.addLayout(leftLayout)
generalLayout.addLayout(rightLayout)
container = QWidget()
container.setLayout(generalLayout)
self.setCentralWidget(container)
def setTable(self, rows):
self.table.setRowCount(rows)
# self.table.setFixedHeight(self.tableHeight())
def tableWidth(self):
tableWidth = self.table.verticalHeader().width() + \
self.table.horizontalHeader().length() + \
self.table.frameWidth() * 2
app.processEvents() # Otherwise verticalScrollBar().isVisible()
app.processEvents() # lags one event
if self.table.verticalScrollBar().isVisible():
tableWidth += self.table.verticalScrollBar().width()
return tableWidth
def tableHeight(self):
tableHeight = self.table.verticalHeader().length() + \
self.table.horizontalHeader().height() + \
self.table.frameWidth() * 2
return tableHeight
app = QApplication(os.sys.argv)
mywindow = MainWindow()
mywindow.show()
app.exec()
I tried playing with different size policies, but all of them give the behavior mentioned above.
I had a vague idea about placing the table in a label, but I don't know how it could help.
Additionally, one other behavior I don't understand is why adding top or bottom alignment for the button under the table makes available space half the height of the window. As if because of that the QVBoxLayout starts distributing space evenly between it's two items.
I think getting the value of the available space in a given window geometry migth help me adjust table size manually. So at least an advise how to do that could help me.
I use Python 3.9.12 and PyQt 6.3.0

PySide2 QTextEdit doesn't adjust to it's own content when using wrapping. (Making chat window)

The PySide2 QTextEdit doesn't change it's own size when the text is placed in.
I'm trying to create something like chat window, where every message - QTextEdit in OnlyRead mode. All the 'messages' placed in QScrollArea. The main goal is to let message-boxes (message-boxeslike on the screen below) adjust their size to content.
wrong working example
I tried this code
https://ru.stackoverflow.com/questions/1408239/Как-сделать-двухсторонний-чат-в-qt-pyqt/1408264#1408264
which has been copy-pasted lots of times. But it doesn't do what i want. It creates a fixed, no resizable QTextEdit message-boxes.
As example what i actually mean, if we have a single-word message, QTextEdit widget must become a single stroke box, with width of the message. If we have a multi-sentences message, QTextEdit widget must become a multi-stroke box (already expanded in height, without the need to scroll it inside), with maximum constant length(which i ll choose).
Next is the example with correct messages displaying
(good example)
In order to implement a self-adjusting message, some precautions are required.
As explained in my answer and comments to the related post, you must consider the complex and delicate relation between the dimensions of a layout and its requirement, which becomes even more complex as a text layout doesn't have a fixed ratio.
The main problem with setting the width based on the text is that it can change the height required to display it, which can cause a recursion, and since the size is based on the current viewport size, the result is that the minimumSizeHint() will always return the smallest possible size after a certain amount of recursive calls.
Considering the above, we must do the following changes to my original code:
the scroll area must always set a maximum width to its message widgets, possibly with a specified margin (for sent/received distinction) whenever the view is resized;
widgets must be added to the layout with an alignment argument;
the minimumSizeHint() must be changed to:
compute the preferred text width (idealWidth() based on the maximum size of the widget;
get the reference height for that text width;
set the text width to the current width;
compare the new document height with the previous one, if they are the same it means that we can use the new width as maximum width for the hint (the text can be shorter), otherwise we use the initial text width based on the maximum size;
Note that there are a couple of differences from the modified code of your link: most importantly, rewriting the stylesheet doesn't make a lot of sense, and setting the margin creates an issue with the value returned by frameWidth() (that's why they subtracted 100 from the document height); that is certainly not a good choice, as the margin should be set within the layout.
class WrapLabel(QtWidgets.QTextEdit):
def __init__(self, text=''):
super().__init__(text)
self.setReadOnly(True)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Maximum)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.textChanged.connect(self.updateGeometry)
def minimumSizeHint(self):
margin = self.frameWidth() * 2
doc = self.document().clone()
doc.setTextWidth(self.maximumWidth())
idealWidth = doc.idealWidth()
idealHeight = doc.size().height()
doc.setTextWidth(self.viewport().width())
if doc.size().height() == idealHeight:
idealWidth = doc.idealWidth()
return QtCore.QSize(
max(50, idealWidth + margin),
doc.size().height() + margin)
def sizeHint(self):
return self.minimumSizeHint()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateGeometry()
class ChatTest(QtWidgets.QScrollArea):
def __init__(self):
super().__init__()
self.margin = 100
self.marginRatio = .8
self.messages = []
container = QtWidgets.QWidget()
self.setWidget(container)
self.setWidgetResizable(True)
layout = QtWidgets.QVBoxLayout(container)
layout.addStretch()
self.resize(480, 360)
letters = 'abcdefghijklmnopqrstuvwxyz '
for i in range(1, 11):
msg = ''.join(choice(letters) for i in range(randrange(10, 250)))
QtCore.QTimer.singleShot(500 * i, lambda msg=msg, i=i:
self.addMessage(msg, i & 1))
def addMessage(self, text, sent=False):
message = WrapLabel(text)
message.setStyleSheet('''
WrapLabel {{
border: 1px outset palette(dark);
border-radius: 8px;
background: {};
}}
'''.format(
'#fff8c7' if sent else '#ceffbd')
)
self.messages.append(message)
self.widget().layout().addWidget(message,
alignment=QtCore.Qt.AlignRight if sent else QtCore.Qt.AlignLeft)
QtCore.QTimer.singleShot(0, self.scrollToBottom)
def scrollToBottom(self):
QtWidgets.QApplication.processEvents()
self.verticalScrollBar().setValue(
self.verticalScrollBar().maximum())
def resizeEvent(self, event):
sb = self.verticalScrollBar()
atMaximum = sb.value() == sb.maximum()
maxWidth = max(self.width() * self.marginRatio,
self.width() - self.margin) - sb.sizeHint().width()
for message in self.messages:
message.setMaximumWidth(maxWidth)
super().resizeEvent(event)
if atMaximum:
sb.setValue(sb.maximum())

Control location on screen where new windows open with graphics.py

I have a python program that deploys a windows via graphics.py. The initial window opened by the GraphWin class opens in the top left corner of the screen. Subsequent calls to GraphWin cascade from the upper left to the lower right.
I'd like to control the placement of each window. (Example: Have all the windows open in a grid-layout so I can create a dashboard.)
I think there is no such method in graphics.py right now.
Ref: The Book and webpage.
If you want to stick to using graphics.py, I suggest creating a dashboard by dividing a single window into different slots.
This option does exist in Tkinter library. Please refer to this answer for more information on that.
graphics.py doesn't provide a way for you to control the location of instances of its GraphWin class. However the fact that it's built on top of Python's Tk GUI toolkit module named tkinter means that sometimes you can work around its limitations by looking at its source code to see how things operate internally.
For example, here's a snippet of code from the module (version 5.0) showing the beginning of GraphWin class' definition from the graphics.py file:
class GraphWin(tk.Canvas):
"""A GraphWin is a toplevel window for displaying graphics."""
def __init__(self, title="Graphics Window",
width=200, height=200, autoflush=True):
assert type(title) == type(""), "Title must be a string"
master = tk.Toplevel(_root)
master.protocol("WM_DELETE_WINDOW", self.close)
tk.Canvas.__init__(self, master, width=width, height=height,
highlightthickness=0, bd=0)
self.master.title(title)
self.pack()
master.resizable(0,0)
self.foreground = "black"
self.items = []
self.mouseX = None
self.mouseY = None
self.bind("<Button-1>", self._onClick)
self.bind_all("<Key>", self._onKey)
self.height = int(height)
self.width = int(width)
self.autoflush = autoflush
self._mouseCallback = None
self.trans = None
self.closed = False
master.lift()
self.lastKey = ""
if autoflush: _root.update()
As you can see it's derived from a tkinter.Canvas widget which has an attribute named master which is a tkinter.Toplevel widget. It then initializes the Canvas base class and specifies the newly created Toplevel window as its parent.
The size and position of a Toplevel window can be controlled by calling its geometry() method as described in the linked documentation. This method expects to be passed a "geometry string" argument in a certain format ('wxh±x±y').
This mean you can take advantage of how this implementation detail in order to put it anywhere you want it and as well as resize if desired.
Here's an example of doing that:
from graphics import *
def main():
win = GraphWin("My Circle", 100, 100)
# Override size and position of the GraphWin.
w, h = 300, 300 # Width and height.
x, y = 500, 500 # Screen position.
win.master.geometry('%dx%d+%d+%d' % (w, h, x, y))
c = Circle(Point(50,50), 10)
c.draw(win)
win.getMouse() # pause for click in window
win.close()
if __name__ == '__main__':
main()
My desktop while script is running:

A QWidget like QTextEdit that wraps its height automatically to its contents?

I am creating a form with some QTextEdit widgets.
The default height of the QTextEdit exceeds a single line of text and as the contents' height exceeds the QTextEdit's height, it creates a scroll-bar to scroll the content.
I would like to override this behaviour to create a QTextEdit that would rather wrap its height to its contents. This means that the default height would be one line and that on wrapping or entering a new line, the QTextEdit would increase its height automatically. Whenever the contents height exceeds the QTextEdit's height, the latter should not create a scroll bar but simply increase in height.
How can I go about doing this? Thanks.
This is almost exactly like a question I answer the other day about making a QTextEdit adjust its height in reponse to content changes: PySide Qt: Auto vertical growth for TextEdit Widget
I am answering instead of marking a duplicate as I suspect its possible you want a variation on this. Let me know if you want me to expand this answer:
The other question had multiple parts. Here is the excerpt of the growing height widget:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
the following code sets a QTextEdit widget to the height of the content:
# using QVBoxLayout in this example
grid = QVBoxLayout()
text_edit = QTextEdit('Some content. I make this a little bit longer as I want to see the effect on a widget with more than one line.')
# read-only
text_edit.setReadOnly(True)
# no scroll bars in this example
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# you can set the width to a specific value
# text_edit.setFixedWidth(400)
# this is the trick, we nee to show the widget without making it visible.
# only then the document is created and the size calculated.
# Qt.WA_DontShowOnScreen = 103, PyQt does not have this mapping?!
text_edit.setAttribute(103)
text_edit.show()
# now that we have a document we can use it's size to set the QTextEdit's size
# also we add the margins
text_edit.setFixedHeight(text_edit.document().size().height() + text_edit.contentsMargins().top()*2)
# finally we add the QTextEdit to our layout
grid.addWidget(text_edit)
I hope this helps.

Placing child window relative to parent in Tkinter python

I have a parent widget which contains a button. When the button is pressed I would like to open a borderless (i.e. no Windows decoration buttons) window directly underneath the parent widget aligned to the left hand side of it. I'm puzzled that the only way (it seems) of setting the position of a window is using .geometry() but worse, I can't seem to get the absolute coordinates of the parent widget - which I need for .geometry(), only the offsets from the parent's parent. So far my code is:
# This is the child which appears when the button is pressed.
class ChildPopUpWindow(Frame):
def __init__(self, parentWgdt):
win = Toplevel(parentWgdt)
geom = str(parentWgdt.winfo_x()) + '+' + str(parentWgdt.winfo_y() + parentWgdt.winfo_height())
win.overrideredirect(1) # No win decoration.
win.bd = 10
win.relief = GROOVE
win.geometry( geom )
Frame.__init__(self, win)
# etc. etc.
# ... and this is the handler for the button being pressed.
def onDropDown(self):
popUp = ChildPopUpWindow(self)
This does pop up a window but relative to the desktop, not to the parent widget. It also seems to take no account of the border thickness and relief as far as I can see. Can anyone offer a way that this can be done? Is .geometry() the way to go or are there better ways?
The short answer is, use winfo_rootx and winfo_rooty to get the coordinates relative to the screen. And yes, wm_geometry is the way to place a toplevel window precisely.
For example:
x = parentWgdt.winfo_rootx()
y = parentWgdt.winfo_rooty()
height = parentWgdt.winfo_height()
geom = "+%d+%d" % (x,y+height)
As a bit of friendly advice, I recommend against abbrev var nms. It makes the code hard to read, especially when the abbreviation is wrong (Wgdt should at least be Wdgt). The difference in code size between geom and geometry, and Wgdt and Widget are tiny, but the difference in readability is huge.
According to Tk manual "https://www.tcl.tk/man/tcl8.4/TkCmd/winfo.htm#M52"
If you need the true width immediately after creating a widget, invoke update to force the geometry manager to arrange it, or use winfo reqwidth to get the window's requested width instead of its actual width.
# This code works perfectly
self.update()
self.geometry("+%d+%d" % (self.parent.winfo_rootx()+50,
self.parent.winfo_rooty()+50
)
)
to centring a modal window about a its parent window, I do so:
alto_modal = 100
ancho_modal = 250
alto_parent = parent.winfo_height()
ancho_parent = parent.winfo_width()
x = (ancho_parent - ancho_modal) // 2
y = (alto_parent - alto_modal) // 2
self.geometry('{}x{}+{}+{}'.format(ancho_modal, alto_modal, x, y))

Categories