I am trying to build a simple code/text editor in PyQt5. Three important functionalities of a code-editor come to my mind:
(1) Syntax highlighting
(2) Code completion
(3) Clickable functions and variables
I plan to base the code editor on a QPlainTextEdit widget. For the syntax highlighting, I will use the QSyntaxHighlighter:
https://doc.qt.io/qt-5/qsyntaxhighlighter.html#details
I have not yet figured out how to do code completion, but will worry about that feature later on. The showstopper right now is 'clickable functions and variables'. In mature code editors, one can click on a function call and jump to the definition of that function. The same holds for variables.
I know that asking "how do I implement this feature?" is way too broad. So let's narrow it down to the following problem:
How can you make a certain word clickable in a QPlainTextEdit widget? An arbitrary Python function should get called when the word gets clicked. That Python function should also know what word was clicked to take appropriate action. Making the word light up blue-ish when the mouse hovers over it would be a nice bonus.
I have written a small test-code, so you have a Qt window with a QPlainTextEdit widget in the middle to play around with:
Here is the code:
import sys
import os
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
##################################################
# 'testText' is a snippet of C-code that #
# will get displayed in the text editor. #
##################################################
testText = '\
# include <stdio.h>\n\
# include <stdbool.h>\n\
# include <stdint.h>\n\
# include "../FreeRTOS/Kernel/kernel.h"\n\
\n\
int main(void)\n\
{\n\
/* Reset all peripherals*/\n\
HAL_Init();\n\
\n\
/* Configure the system clock */\n\
SystemClock_Config();\n\
\n\
/* Initialize all configured peripherals */\n\
MX_GPIO_Init();\n\
MX_SPI1_Init();\n\
MX_SPI2_Init();\n\
MX_SPI3_Init();\n\
}\n\
'
##################################################
# A simple text editor #
# #
##################################################
class MyMainWindow(QMainWindow):
def __init__(self):
super(MyMainWindow, self).__init__()
# Define the geometry of the main window
self.setGeometry(200, 200, 800, 800)
self.setWindowTitle("text editor test")
# Create center frame
self.centerFrm = QFrame(self)
self.centerFrm.setStyleSheet("QWidget { background-color: #ddeeff; }")
self.centerLyt = QVBoxLayout()
self.centerFrm.setLayout(self.centerLyt)
self.setCentralWidget(self.centerFrm)
# Create QTextEdit
self.myTextEdit = QPlainTextEdit()
self.myTextEdit.setPlainText(testText)
self.myTextEdit.setStyleSheet("QPlainTextEdit { background-color: #ffffff; }")
self.myTextEdit.setMinimumHeight(500)
self.myTextEdit.setMaximumHeight(500)
self.myTextEdit.setMinimumWidth(700)
self.myTextEdit.setMaximumWidth(700)
self.centerLyt.addWidget(self.myTextEdit)
self.show()
if __name__== '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Fusion'))
myGUI = MyMainWindow()
app.exec_()
del app
sys.exit()
EDIT
I just revisited this question after long time. In the meantime, I've constructed a website about QScintilla: https://qscintilla.com
You can find all info there :-)
Maybe you can utilize qutepart. It looks like it does what you need. If you can't use it, then maybe you can look at the source code to see how they implemented different features, like clickable text.
Related
I've got a small example of the GUI I'm working on in Qt6, that has a problem switching palette colors (to switch from dark to light theme). When I apply my changes to QPalette to change the text color, they only work when the window is inactive. Weirdly, if I remove the font-family specification from the stylesheet then the color change works properly. This all works fine in Qt5 without any messing around.
On load, the GUI looks fine
After clicking the "Change Theme" button, it looks fine except that the text color setting that I change using Palette does not work (it's still black)
If I click on my desktop or a different window to make my GUI inactive, it then shows the correct text color (red)
Light - Working, Dark - Broken, Dark - Working
Any workaround suggestions (that make color and font both always work correctly) are welcome, but I'd love to know what I'm actually doing wrong here, and why it used to work in Qt5 and doesn't in Qt6! Thanks!
from PyQt6 import QtWidgets
from PyQt6.QtGui import QPalette, QColor, QFont
APP = QtWidgets.QApplication([])
class UiMain(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setObjectName("MainWindow")
central_widget = QtWidgets.QWidget(self)
self.setCentralWidget(central_widget)
tabs = QtWidgets.QTabWidget(central_widget)
vertical_layout_26 = QtWidgets.QVBoxLayout(central_widget)
vertical_layout_26.addWidget(tabs)
search_tab = QtWidgets.QWidget()
tabs.addTab(search_tab, "")
tabs.setTabText(tabs.indexOf(search_tab), "Search")
filter_group_box = QtWidgets.QGroupBox(search_tab)
filter_group_box.setTitle("Filters")
self.theme_btn = QtWidgets.QPushButton()
self.theme_btn.setText("Change Theme")
searchbar_layout = QtWidgets.QHBoxLayout(search_tab)
searchbar_layout.addWidget(QtWidgets.QLabel("asdf"))
searchbar_layout.addWidget(filter_group_box)
searchbar_layout.addWidget(self.theme_btn)
class View(UiMain):
def __init__(self):
super().__init__()
self.theme_btn.clicked.connect(self.change_theme) # noqa
# Create Palettes
self.light_palette = QPalette()
self.dark_palette = QPalette()
self.dark_palette.setColor(QPalette.ColorRole.WindowText, QColor("red"))
# # This didn't help
# self.dark_palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText, QColor("red"))
# Create Stylesheets
self.style_light = """
* {font-family: 'Noto Sans';} /* REMOVING THIS LINE AVOIDS THE ISSUE, BUT THEN FONTS ARE WRONG INITIALLY */
QMainWindow {background-color: white;}
"""
self.style_dark = """
* {font-family: 'Noto Sans';}
QMainWindow {background-color: gray;}
"""
# Set initial theme
self.dark = False
APP.setPalette(self.light_palette)
APP.setStyleSheet(self.style_light)
self.show()
def change_theme(self):
"""Allow user to switch between dark and light theme"""
if self.dark:
self.dark = False
APP.setPalette(self.light_palette)
APP.setStyleSheet(self.style_light)
else:
self.dark = True
APP.setPalette(self.dark_palette)
APP.setStyleSheet(self.style_dark)
if __name__ == '__main__':
gui = View()
APP.exec()
I ran into this as well using PySide6 (occurs both on Windows and Linux).
In my case, the issue was that changing the palette using QApplication.setPalette after I had done anything with stylesheets resulted in the stylesheet not being properly applied to existing windows / objects or their children. Deleting the window and creating a new one worked as intended.
The issue (in my case) can be seen using something like the following. The application's palette would show my text color from my palette, but the window's palette shows the default black text color.
# Change theme
my_app.setStyleSheet(new_stylesheet)
my_app.setStyle(new_style)
my_app.setPalette(new_palette)
# self is a QMainWindow that was open when the theme was changed
print(QApplication.palette().color(QPalette.Text).name())
print(self.palette().color(QPalette.Text).name())
I do not know if this is a bug or not, but I was able to work around it without creating a new window instance by manually (and recursively) applying the palette to my existing window and its children using something like the following function
def change_palette_recursive(root: QWidget, palette: QPalette):
root.setPalette(palette)
for child in root.children():
if isinstance(child, QWidget):
change_palette_recursive(child, palette)
This should be called after changing the theme. It likely needs to be called on each open window (unless it is a child of another open window).
change_theme()
change_palette_recursive(existing_window, QApplication.palette())
I would generally consider this sort of thing bad form, but it is the only thing I have found (so far) that works.
I'm trying to take a screenshot of the current active window in PyQt5. I know the generic method to take an screenshot of any window is QScreen::grabWindow(winID), for which winID is an implementation-specific ID depending on the window system. Since I'm running X and KDE, I plan to eventual use CTypes to call Xlib, but for now, I simply execute "xdotool getactivewindow" to obtain the windowID in a shell.
For a minimum exmaple, I created a QMainWindow with a QTimer. When the timer is fired, I identify the active window ID by executing "xdotool getactivewindow", get its return value, call grabWindow() to capture the active window, and display the screetshot in a QLabel. On startup, I also set my window a fixed 500x500 size for observation, and activate Qt.WindowStaysOnTopHint flag, so that my window is still visible when it's not in focus. To put them together, the implementation is the following code.
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
self.screen = QtWidgets.QApplication.primaryScreen()
#QtCore.pyqtSlot()
def timer_handler(self):
window = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
self.screenshot = self.screen.grabWindow(window)
self.label.setPixmap(self.screenshot)
self.label.setFixedSize(self.screenshot.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
To test the implementation, I started the script and clicked another window. It appears to work without problems if there is no overlap between my application window and the active window. See the following screenshot, when Firefox (right) is selected, my application is able to capture the active window of Firefox and display it in the QLabel.
However, the screenshot doesn't work as expected if there is an overlap between the application window and the active window. The window of the application itself will be captured, and creates a positive feedback.
If there is an overlap between the application window and the active window. The window of the application itself will be captured, and creates a positive feedback.
I've already disabled the 3D composite in KDE's settings, but the problem remains. The examples above are taken with all composite effects disabled.
Question
Why isn't this implementation working correctly when the application window and the active window are overlapped? I suspect it's an issue caused by some forms of unwanted interaction between graphics systems (Qt toolkit, window manager, X, etc), but I'm not sure.
Is it even possible solve this problem? (Note: I know I can hide() before the screenshot and show() it again, but it doesn't really solve this problem, which is taking a screenshot even if an overlap exists.)
As pointed out by #eyllanesc, it appears that it is not possible to do it in Qt, at least not with QScreen::grabWindow, because grabWindow() doesn't actually grab the window itself, but merely the area occupied by the window. The documentation contains the following warning.
The grabWindow() function grabs pixels from the screen, not from the window, i.e. if there is another window partially or entirely over the one you grab, you get pixels from the overlying window, too. The mouse cursor is generally not grabbed.
The conclusion is that it's impossible do to it in pure Qt. It's only possible to implement such a functionality by writing a low-level X program. Since the question asks for a solution "in Qt", any answer that potentially involves deeper, low-level X solutions are out-of-scope. This question can be marked as resolved.
The lesson to learn here: Always check the documentation before using a function or method.
Update: I managed to solve the problem by reading the window directly from X via Xlib. Somewhat ironically, my solution uses GTK to grab the window and sends its result to Qt... Anyway, you can write the same program with Xlib directly if you don't want to use GTK, but I used GTK since the Xlib-related functions in GDK is pretty convenient to demonstrate the basic concept.
To get a screenshot, we first convert our window ID to an GdkWindow suitable for use within GDK, and we call Gdk.pixbuf_get_from_window() to grab the window and store it in a gdk_pixbuf. Finally, we call save_to_bufferv() to convert the raw pixbuf to a suitable image format and store it in a buffer. At this point, the image in the buffer is suitable to use in any program, including Qt.
The documentation contains the following warning:
If the window is off the screen, then there is no image data in the obscured/offscreen regions to be placed in the pixbuf. The contents of portions of the pixbuf corresponding to the offscreen region are undefined.
If the window you’re obtaining data from is partially obscured by other windows, then the contents of the pixbuf areas corresponding to the obscured regions are undefined.
If the window is not mapped (typically because it’s iconified/minimized or not on the current workspace), then NULL will be returned.
If memory can’t be allocated for the return value, NULL will be returned instead.
It also has some remarks about compositing,
gdk_display_supports_composite has been deprecated since version 3.16 and should not be used in newly-written code.
Compositing is an outdated technology that only ever worked on X11.
So basically, it's only possible to grab a partially obscured window under X11 (not possible in Wayland!), with a compositing window manager. I tested it without compositing, and found the window is blacked-out when compositing is disabled. But when composition is enabled, it seems to work without problem. It may or may not work for your application. But I think if you are using compositing under X11, it probably will work.
from PyQt5 import QtCore, QtGui, QtWidgets
import subprocess
class ScreenCapture(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
self.setFixedHeight(500)
self.setFixedWidth(500)
self.label = QtWidgets.QLabel(self)
self.screen = QtWidgets.QApplication.primaryScreen()
self.timer = QtCore.QTimer(self)
self.timer.setInterval(500)
self.timer.timeout.connect(self.timer_handler)
self.timer.start()
#staticmethod
def grab_screenshot():
from gi.repository import Gdk, GdkX11
window_id = int(subprocess.check_output(["xdotool", "getactivewindow"]).decode("ascii"))
display = GdkX11.X11Display.get_default()
window = GdkX11.X11Window.foreign_new_for_display(display, window_id)
x, y, width, height = window.get_geometry()
pb = Gdk.pixbuf_get_from_window(window, 0, 0, width, height)
if pb:
buf = pb.save_to_bufferv("bmp", (), ())
return buf[1]
else:
return
#QtCore.pyqtSlot()
def timer_handler(self):
screenshot = self.grab_screenshot()
self.pixmap = QtGui.QPixmap()
if not self.pixmap:
return
self.pixmap.loadFromData(screenshot)
self.label.setPixmap(self.pixmap)
self.label.setFixedSize(self.pixmap.size())
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = ScreenCapture()
window.show()
app.exec()
Now it captures an active window perfectly, even if there are overlapping windows on top of it.
I discovered a very strange behavior which causes blinking (white rectangle is shown as the main window for about half a second) at application start up in some cases. After a long time of test and trial I narrowed down the problem to the situation when a text is first drawn in paint event of my custom widget. If a text is drawn also in another widget such as QLabel, the problem is gone. But my application main window only has tool buttons with icons and a custom widget which draws text, no other widgets. The white blinking is very ugly and I would like to get rid of it, ideally with some proper solution and without nasty hacks of introducing some artificial text drawing widgets. Moreover I am not very comfortable because I really do not know what is actually going on. Why the custom widget causes blinking and QLabel not? To prove the behavior try the following code (the same problem is in C++/Qt so it is not caused by Python wrapper). Then try to uncomment the marked line and comment the next one to see that the blinking is gone.
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel
class CustomWidget(QWidget):
def paintEvent(self, event):
p = QPainter(self)
p.drawText(20, 20, "XYZ")
app = QApplication([])
container = QWidget()
layout = QVBoxLayout(container)
# label = QLabel("ABC") # uncomment this to prevent blinking
label = QLabel() # comment this out to prevent blinking
layout.addWidget(label)
layout.addWidget(CustomWidget())
container.resize(600, 600)
container.show()
app.exec()
Any ideas what is going on there? I am using Qt 5.9.2.
I have an application where I want the QTabBar to be in a separate VBoxLayout from the QTabWidget area. It sort of works using the code below but I'm having styling problems. Before I separated the QTabBar from the QTabWidget I didn't have any problems but now I can't figure out how to style it the way I want.
#!/usr/bin/env python2
from PyQt4 import QtGui, QtCore
from peaks import *
class mainWindow(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.setWindowFlags(QtCore.Qt.Dialog)
self.tabWidget = QtGui.QTabWidget()
self.tabBar = QtGui.QTabBar()
self.tabBar.setContentsMargins(0,0,0,0)
self.tabWidget.setTabBar(self.tabBar)
self.tabWidget.setTabPosition(QtGui.QTabWidget.West)
self.tabWidget.setIconSize(QtCore.QSize(35, 35))
self.tab1 = QtGui.QWidget()
self.tab2 = QtGui.QWidget()
tabLayoutBox = QtGui.QVBoxLayout()
tabLayoutBox.setContentsMargins(0,0,0,0)
tabLayoutBox.addWidget(self.tabBar)
mainHBox = QtGui.QHBoxLayout()
mainHBox.setContentsMargins(0,0,0,0)
mainHBox.setSpacing(0)
mainHBox.setMargin(0)
mainHBox.addLayout(tabLayoutBox)
mainHBox.addWidget(self.tabWidget)
mainVBox = QtGui.QVBoxLayout()
mainVBox.addWidget(QtGui.QWidget())
mainVBox.addLayout(mainHBox)
self.setLayout(mainVBox)
self.tabWidget.addTab(self.tab1, 'tab1')
self.tabWidget.addTab(self.tab2, 'tab2')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
app.setStyleSheet(
"QTabBar { alignment: right; }"
"QTabBar::tear { width:0; border: none; }"
"QTabBar::scroller { width:0; border: none; }"
)
main = mainWindow()
main.show()
sys.exit(app.exec_())
However there are a couple of things that I want that I can't figure out how to do:
I want to eliminate the gap between the QTabWidget and the QTabBar. I've been trying various things like setContentsMargins(0,0,0,0) and setting stylesheets but nothing I've tried has worked.
I want QTabBar to be flush with the top of the QTabWidget. Interesting to note that the tabs seem to rapidly switch back and forth between the top whenever the window is resized.
stuff I've looked at:
Qt Use QTabBar in a Different QLayout
http://doc.qt.io/qt-5/stylesheet-examples.html
https://wiki.qt.io/Adjust_Spacing_and_Margins_between_Widgets_in_Layout
update: I can emulate my desired behavior by setting the QTabBar miminumSizeHint() to (15,15) and setting QTabBar::tab { margin-right: -15px; } but this doesn't let me actually click the tabs. there's a space underneath (ie to the right of) the tabs for some reason and I've no idea how to get rid of it.
second update: I've identified the main problem I think. my code uses
self.tabWidget.setTabPosition(QtGui.QTabWidget.West)
to move the tab to the left side but the QTabWidget assumes that there is a tabBar there, hence the extra space. If I do
self.tabWidget.setTabPosition(QtGui.QTabWidget.East)
that blank space shows up at the right. So one thing I can do is set the tabShape directly on the tabBar:
self.tabBar.setShape(QtGui.QTabBar.RoundedWest)
however this leaves a blank space at the top where the QTabWidget expects the QTabBar to be. I can move that space to the right using setTabPosition before setShape but that doesn't solve the problem of actually getting rid of it.
I wasn't able to figure out how to hide the empty space so instead I'm just using a QTabBar + QStackedWidget which is quite easy to implement. In order to make one like a QTabWidget all you need to do is connect QTabBar.currentChanged to QStackedWidget.setCurrentIndex:
self.stacked = QtGui.QStackedWidget()
self.tabBar = QtGui.QTabBar()
self.tabBar.currentChanged.connect(self.stacked.setCurrentIndex)
self.tabBar.setShape(QtGui.QTabBar.RoundedWest)
self.tabBar.updateGeometry()
self.tabBar.setContentsMargins(0,0,0,0)
I also wrote a convenience function that emulates QTabWidget.addTab:
def addTab(self, widget, name):
self.stacked.addWidget(widget)
self.tabBar.addTab(name)
Your problem is that you override the tab bar positioning by adding the tab bar to a layout.
The tab bar is no longer in the tab widget when you use the lines.
tabLayoutBox = QtGui.QVboxLayout()
tabLayoutBox.addWidget(self.tabBar)
These lines re-parent the tab bar. It looks like you just want to use setTabPosition() instead of creating your own tab bar. You don't need to set a new tab bar unless you create a custom tab bar class and want to use that.
I don't know why you would want it in a separate layout, but the other option is use
tabLayoutBox.setSpacing(0)
This is the spacing in between widgets to separate them. That spacing is for widgets. You have a layout in a layout, so setSpacing(0) may not apply to the spacing on a layout. If not you may have to sub-class QVBoxLayout and create your own layout.
EDIT
I've found that insertSpacing works a lot better.
mainHBox.insertSpacing(1, -25)
I am developing an application; which will run on a system with 2 displays. I want my PyQt app to be able to automatically route a particular window to the second screen.
How can this be done in Qt? (either in Python or C++)
The following works for me in PyQt5
import sys
from PyQt5.QtWidgets import QApplication, QDesktopWidget
app = QApplication(sys.argv)
widget = ... # define your widget
display_monitor = ... # the number of the monitor you want to display your widget
monitor = QDesktopWidget().screenGeometry(display_monitor)
widget.move(monitor.left(), monitor.top())
widget.showFullScreen()
Update. In PyQt6 one should use:
...
from PyQt6.QtGui import QScreen
...
monitors = QScreen.virtualSiblings(widget.screen())
monitor = monitors[display_monitor].availableGeometry()
...
Monitors should be counted starting from 0.
Use QDesktopWidget to access to screen information on multi-head systems.
Here is pseudo code to make a widget cover first screen.
QDesktopWidget *pDesktop = QApplication::desktop ();
//Get 1st screen's geometry
QRect RectScreen0 = pDesktop->screenGeometry (0);
//Move the widget to first screen without changing its geometry
my_Widget->move (RectScreen0.left(),RectScreen0.top());
my_pWidget->resize (RectScreen0.width(),RectScreen0.height());
my_Widget->showMaximized();