Python GUIshttps://www.pythonguis.com/2024-03-27T06:00:00+00:00Create GUI applications with Python and QtQ&A: How Do I Display Images in PySide6? — Using QLabel to easily add images to your applications2024-03-27T06:00:00+00:002024-03-27T06:00:00+00:00John Limtag:www.pythonguis.com,2024-03-27:/faq/adding-images-to-pyside6-applications/<p>Adding images to your application is a common requirement, whether you're building an image/photo viewer, or just want to add some decoration to your GUI. Unfortunately, because of how this is done in Qt, it can be a little bit tricky to work out at first.</p>
<p>In this short tutorial, we will look at how you can insert an external image into your PySide6 application layout, using both code and Qt Designer.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#which-widget-to-use">Which widget to use?</a></li>
<li><a href="#using-qt-designer">Using Qt Designer</a></li>
<li><a href="#using-code">Using Code</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h3 id="which-widget-to-use">Which widget to use?</h3>
<p>Since you're wanting to insert an image you might be expecting to use a widget named <code>QImage</code> or similar, but that would make a bit too much sense! <code>QImage</code> is actually Qt's image <em>object</em> type, which is used to store the actual image data for use within your application. The <em>widget</em> you use to display an image is <code>QLabel</code>.</p>
<p>The primary use of <code>QLabel</code> is of course to add labels to a UI, but it also has the ability to display an image — or <em>pixmap</em> — instead, covering the entire area of the widget. Below we'll look at how to use <code>QLabel</code> to display a widget in your applications.</p>
<h3 id="using-qt-designer">Using Qt Designer</h3>
<p>First, create a <em>MainWindow</em> object in Qt Designer and add a "Label" to it. You can find Label at in <em>Display Widgets</em> in the bottom of the left hand panel. Drag this onto the <code>QMainWindow</code> to add it.</p>
<p><img alt="MainWindow with a single QLabel added" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/1.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-600 600w" loading="lazy" width="1917" height="1027"/>
<em>MainWindow with a single QLabel added</em></p>
<p>Next, with the Label selected, look in the right hand <code>QLabel</code> properties panel for the <code>pixmap</code> property (scroll down to the blue region). From the property editor dropdown select "Choose File…" and select an image file to insert.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442141?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>As you can see, the image is inserted, but the image is kept at its original size, cropped to the boundaries of the<code>QLabel</code> box. You need to resize the <code>QLabel</code> to be able to see the entire image.</p>
<p>In the same controls panel, click to enable <code>scaledContents</code>.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442184?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>When <code>scaledContents</code> is enabled the image is resized to the fit the bounding box of the <code>QLabel</code> widget. This shows the entire image at all times, although it does not respect the aspect ratio of the image if you resize the widget.</p>
<p>You can now save your UI to file (e.g. as <code>mainwindow.ui</code>).</p>
<p>To view the resulting UI, we can use the standard application template below. This loads the <code>.ui</code> file we've created (<code>mainwindow.ui</code>) creates the window and starts up the application.</p>
<div class="code-block">
<span class="code-block-language code-block-PySide6">PySide6</span>
<pre><code class="PySide6">import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader()
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
window.show()
app.exec()
</code></pre>
</div>
<p>Running the above code will create a window, with the image displayed in the middle.</p>
<p><img alt="QtDesigner application showing a Cat" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/5.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-600 600w" loading="lazy" width="802" height="639"/>
<em>QtDesigner application showing a Cat</em></p>
<h3 id="using-code">Using Code</h3>
<p>Instead of using Qt Designer, you might also want to show an image in your application through code. As before we use a <code>QLabel</code> widget and add a <em>pixmap</em> image to it. This is done using the <code>QLabel</code> method <code>.setPixmap()</code>. The full code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-PySide6">PySide6</span>
<pre><code class="PySide6">import sys
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
<p>The block of code below shows the process of creating the <code>QLabel</code>, creating a <code>QPixmap</code> object from our file <code>cat.jpg</code> (passed as a file path), setting this <code>QPixmap</code> onto the <code>QLabel</code> with <code>.setPixmap()</code> and then finally resizing the window to fit the image.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p>Launching this code will show a window with the cat photo displayed and the window sized to the size of the image.</p>
<p><img alt="QMainWindow with Cat image displayed" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/4.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-600 600w" loading="lazy" width="602" height="439"/>
<em>QMainWindow with Cat image displayed</em></p>
<p>Just as in Qt designer, you can call <code>.setScaledContents(True)</code> on your <code>QLabel</code> image to enable scaled mode, which resizes the image to fit the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
label.setScaledContents(True)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Notice that you set the scaled state on the <code>QLabel</code> widget and not the image pixmap itself.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In this quick tutorial we've covered how to insert images into your Qt UIs using <code>QLabel</code> both from Qt Designer and directly from PySide6 code.</p><p>Adding images to your application is a common requirement, whether you're building an image/photo viewer, or just want to add some decoration to your GUI. Unfortunately, because of how this is done in Qt, it can be a little bit tricky to work out at first.</p>
<p>In this short tutorial, we will look at how you can insert an external image into your PySide6 application layout, using both code and Qt Designer.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#which-widget-to-use">Which widget to use?</a></li>
<li><a href="#using-qt-designer">Using Qt Designer</a></li>
<li><a href="#using-code">Using Code</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h3 id="which-widget-to-use">Which widget to use?</h3>
<p>Since you're wanting to insert an image you might be expecting to use a widget named <code>QImage</code> or similar, but that would make a bit too much sense! <code>QImage</code> is actually Qt's image <em>object</em> type, which is used to store the actual image data for use within your application. The <em>widget</em> you use to display an image is <code>QLabel</code>.</p>
<p>The primary use of <code>QLabel</code> is of course to add labels to a UI, but it also has the ability to display an image — or <em>pixmap</em> — instead, covering the entire area of the widget. Below we'll look at how to use <code>QLabel</code> to display a widget in your applications.</p>
<h3 id="using-qt-designer">Using Qt Designer</h3>
<p>First, create a <em>MainWindow</em> object in Qt Designer and add a "Label" to it. You can find Label at in <em>Display Widgets</em> in the bottom of the left hand panel. Drag this onto the <code>QMainWindow</code> to add it.</p>
<p><img alt="MainWindow with a single QLabel added" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/1.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-600 600w" loading="lazy" width="1917" height="1027"/>
<em>MainWindow with a single QLabel added</em></p>
<p>Next, with the Label selected, look in the right hand <code>QLabel</code> properties panel for the <code>pixmap</code> property (scroll down to the blue region). From the property editor dropdown select "Choose File…" and select an image file to insert.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442141?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>As you can see, the image is inserted, but the image is kept at its original size, cropped to the boundaries of the<code>QLabel</code> box. You need to resize the <code>QLabel</code> to be able to see the entire image.</p>
<p>In the same controls panel, click to enable <code>scaledContents</code>.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442184?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>When <code>scaledContents</code> is enabled the image is resized to the fit the bounding box of the <code>QLabel</code> widget. This shows the entire image at all times, although it does not respect the aspect ratio of the image if you resize the widget.</p>
<p>You can now save your UI to file (e.g. as <code>mainwindow.ui</code>).</p>
<p>To view the resulting UI, we can use the standard application template below. This loads the <code>.ui</code> file we've created (<code>mainwindow.ui</code>) creates the window and starts up the application.</p>
<div class="code-block">
<span class="code-block-language code-block-PySide6">PySide6</span>
<pre><code class="PySide6">import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader
loader = QUiLoader()
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
window.show()
app.exec()
</code></pre>
</div>
<p>Running the above code will create a window, with the image displayed in the middle.</p>
<p><img alt="QtDesigner application showing a Cat" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/5.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-600 600w" loading="lazy" width="802" height="639"/>
<em>QtDesigner application showing a Cat</em></p>
<h3 id="using-code">Using Code</h3>
<p>Instead of using Qt Designer, you might also want to show an image in your application through code. As before we use a <code>QLabel</code> widget and add a <em>pixmap</em> image to it. This is done using the <code>QLabel</code> method <code>.setPixmap()</code>. The full code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-PySide6">PySide6</span>
<pre><code class="PySide6">import sys
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
<p>The block of code below shows the process of creating the <code>QLabel</code>, creating a <code>QPixmap</code> object from our file <code>cat.jpg</code> (passed as a file path), setting this <code>QPixmap</code> onto the <code>QLabel</code> with <code>.setPixmap()</code> and then finally resizing the window to fit the image.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p>Launching this code will show a window with the cat photo displayed and the window sized to the size of the image.</p>
<p><img alt="QMainWindow with Cat image displayed" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/4.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-600 600w" loading="lazy" width="602" height="439"/>
<em>QMainWindow with Cat image displayed</em></p>
<p>Just as in Qt designer, you can call <code>.setScaledContents(True)</code> on your <code>QLabel</code> image to enable scaled mode, which resizes the image to fit the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
label.setScaledContents(True)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Notice that you set the scaled state on the <code>QLabel</code> widget and not the image pixmap itself.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In this quick tutorial we've covered how to insert images into your Qt UIs using <code>QLabel</code> both from Qt Designer and directly from PySide6 code.</p>Drag & Drop Widgets with PySide6 — Sort widgets visually with drag and drop in a container2024-03-06T13:00:00+00:002024-03-06T13:00:00+00:00Martin Fitzpatricktag:www.pythonguis.com,2024-03-06:/faq/pyside6-drag-drop-widgets/<p>I had an interesting question from a reader of my <a href="https://www.pythonguis.com/pyside6-book/">PySide6 book</a>, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.</p>
<blockquote>
<p>I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of <code>QPushButton</code>, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.</p>
</blockquote>
<p>First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#drag-drop-widgets">Drag & Drop Widgets</a></li>
<li><a href="#visual-drag-drop">Visual Drag & Drop</a></li>
<li><a href="#generic-drag-drop-container">Generic Drag & Drop Container</a></li>
<li><a href="#adding-a-visual-drop-target">Adding a Visual Drop Target</a></li>
</ul>
</div>
<h2 id="drag-drop-widgets">Drag & Drop Widgets</h2>
<p>We'll start with a simple application which creates a window using <code>QWidget</code> and places a series of <code>QPushButton</code> widgets into it.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can substitute <code>QPushButton</code> for any other widget you like, e.g. <code>QLabel</code>. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = QPushButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this you should see something like this.</p>
<p><img alt="Widgets in a layout" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/windows-in-layout.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>The series of <code>QPushButton widgets</code> in a horizontal layout.</em></p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Here we're creating a window, but the <code>Window</code> widget is subclassed from <code>QWidget</code>, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.</p>
<p><code>QPushButton</code> objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>We implement a <code>mouseMoveEvent</code> which accepts the single <code>e</code> parameter of the event. We check to see if the <em>left</em> mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a <code>QDrag</code> object, passing in <code>self</code> to give us access later to the widget that was dragged. We also <em>must</em> pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.</p>
<p>Finally, we initiate a drag by calling <code>drag.exec_(Qt.MoveAction)</code>. As with dialogs <code>exec_()</code> starts a new event loop, blocking the main loop until the drag is complete. The parameter <code>Qt.MoveAction</code> tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.</p>
<p>You can update the main window code to use our new <code>DragButton</code> class as follows.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run the code now, you <em>can</em> drag the buttons, but you'll notice the drag is forbidden.</p>
<p><img alt="Drag forbidden" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-forbidden.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts but is forbidden.</em></p>
<p>What's happening? The mouse movement is being detected by our <code>DragButton</code> object and the drag started, but the main window does not accept drag & drop.</p>
<p>To fix this we need to enable drops on the window and implement <code>dragEnterEvent</code> to actually accept them.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling <code>drag.exec_()</code>.</p>
<p><img alt="Drag accepted" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-accepted.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts and is accepted, showing a move icon.</em></p>
<p>Releasing the mouse button during a drag drop operation triggers a <code>dropEvent</code> on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our <code>dropEvent</code> method.</p>
<p>The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.</p>
<p>To determine where to place the widget, we iterate over all the widgets in the layout, <em>until</em> we find one who's <code>x</code> position is <em>greater</em> than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.</p>
<p>If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment <code>n</code> one further (in the <code>else:</code> block below).</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x():
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using <code>if pos.x() < w.x() + w.size().width() // 2:</code> -- that is x + half of the width.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The complete working drag-drop code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<h2 id="visual-drag-drop">Visual Drag & Drop</h2>
<p>We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.</p>
<p>Qt's <code>QDrag</code> handler natively provides a mechanism for showing dragged objects which we can use. We can update our <code>DragButton</code> class to pass a <em>pixmap</em> image to <code>QDrag</code> and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a <code>QPixmap</code> of the widget we're dragging.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>To create the pixmap we create a <code>QPixmap</code> object passing in the size of the widget this event is fired on with <code>self.size()</code>. This creates an empty <em>pixmap</em> which we can then pass into <code>self.render</code> to <em>render</em> -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the <code>drag</code> object.</p>
<p>If you run the code with this modification you'll see something like the following --</p>
<p><img alt="Drag visual" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-visual.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget showing the dragged widget.</em></p>
<h2 id="generic-drag-drop-container">Generic Drag & Drop Container</h2>
<p>We now have a working drag and drop behavior implemented on our window.
We can take this a step further and implement a <em>generic</em> drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget <code>DragWidget</code> which can be added to any window.</p>
<p>You can add <em>items</em> -- instances of <code>DragItem</code> -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal <code>orderChanged</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
break
else:
# We aren't on the left hand/upper side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
self.orderChanged.emit(self.get_item_data())
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p><img alt="Generic drag drop horizontal" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-horizontal.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-600 600w" loading="lazy" width="306" height="139"/>
<em>Generic drag-drop sorting in horizontal orientation.</em></p>
<p>You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal <code>QLabel</code> which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call <code>get_item_data</code> yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort <em>anything</em> not just strings.</p>
<p>In the example above we're passing in the enumerated index as the data, so dragging will output (via the <code>print</code> connected to <code>orderChanged</code>) something like:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]
</code></pre>
</div>
<p>If you remove the <code>item.set_data(n)</code> you'll see the labels emitted on changes.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']
</code></pre>
</div>
<p>We've also implemented <em>orientation</em> onto the <code>DragWidget</code> using the Qt built in flags <code>Qt.Orientation.Vertical</code> or <code>Qt.Orientation.Horizontal</code>. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.</p>
<p><img alt="Generic drag drop vertical" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-vertical.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-600 600w" loading="lazy" width="202" height="216"/>
<em>Generic drag-drop sorting in vertical orientation.</em></p>
<h2 id="adding-a-visual-drop-target">Adding a Visual Drop Target</h2>
<p>If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be
inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using <em>guesswork</em> to get it right.</p>
<p>With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.</p>
<p>In this final section we'll implement this type of drag and drop preview indicator.</p>
<p>The first step is to define our target indicator. This is just another label,
which in our example is empty, with custom styles applied to make it have a
solid "shadow" like background. This makes it obviously different to the
items in the list, so it stands out as something distinct.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.</p>
<p>The drag item is unchanged, but we need to implement some additional behavior on our <code>DragWidget</code> to add the target, control showing and moving it.</p>
<p>First we'll add the drag target indicator to the layout on our <code>DragWidget</code>. This is hidden to begin with, but will be shown during the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
</code></pre>
</div>
<p>Next we modify the <code>DragWidget.dragMoveEvent</code> to show the drag target indicator. We show it by <em>inserting</em> it into the layout and then calling <code>.show</code> -- inserting a widget which is already in a layout will move it.
We also hide the original item which is being dragged.</p>
<p>In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.</p>
<p>Instead, the dragged item is left in place and hidden during move.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
</code></pre>
</div>
<p>The method <code>self._find_drop_location</code> finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.</p>
<p>The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
</code></pre>
</div>
<p>The drop location <code>n</code> is returned for use in the <code>dragMoveEvent</code> to place the drop target indicator.</p>
<p>Next wee need to update the <code>get_item_data</code> handler to ignore the drop target indicator. To do this we check <code>w</code> against <code>self._drag_target_indicator</code> and skip if it is the same. With this change the method will work as expected.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
</code></pre>
</div>
<p>If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).</p>
<p>To fix that we need to implement a <code>dragLeaveEvent</code> which hides the indicator.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
</code></pre>
</div>
<p>With those changes, the drag-drop behavior should be working as intended.
The complete code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
def dropEvent(self, e):
widget = e.source()
# Use drop target location for destination, then remove it.
self._drag_target_indicator.hide()
index = self.blayout.indexOf(self._drag_target_indicator)
if index is not None:
self.blayout.insertWidget(index, widget)
self.orderChanged.emit(self.get_item_data())
widget.show()
self.blayout.activate()
e.accept()
def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this example on macOS you may notice that the widget drag preview (the <code>QPixmap</code> created on <code>DragItem</code>) is a bit blurry. On high-resolution screens you need to set the <em>device pixel ratio</em> and scale up the pixmap when
you create it. Below is a modified <code>DragItem</code> class which does this.</p>
<p>Update <code>DragItem</code> to support high resolution screens.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
# Render at x2 pixel ratio to avoid blur on Retina screens.
pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
pixmap.setDevicePixelRatio(2)
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
</code></pre>
</div>
<p>That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p><p>I had an interesting question from a reader of my <a href="https://www.pythonguis.com/pyside6-book/">PySide6 book</a>, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.</p>
<blockquote>
<p>I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of <code>QPushButton</code>, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.</p>
</blockquote>
<p>First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#drag-drop-widgets">Drag & Drop Widgets</a></li>
<li><a href="#visual-drag-drop">Visual Drag & Drop</a></li>
<li><a href="#generic-drag-drop-container">Generic Drag & Drop Container</a></li>
<li><a href="#adding-a-visual-drop-target">Adding a Visual Drop Target</a></li>
</ul>
</div>
<h2 id="drag-drop-widgets">Drag & Drop Widgets</h2>
<p>We'll start with a simple application which creates a window using <code>QWidget</code> and places a series of <code>QPushButton</code> widgets into it.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can substitute <code>QPushButton</code> for any other widget you like, e.g. <code>QLabel</code>. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = QPushButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this you should see something like this.</p>
<p><img alt="Widgets in a layout" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/windows-in-layout.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>The series of <code>QPushButton widgets</code> in a horizontal layout.</em></p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Here we're creating a window, but the <code>Window</code> widget is subclassed from <code>QWidget</code>, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.</p>
<p><code>QPushButton</code> objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>We implement a <code>mouseMoveEvent</code> which accepts the single <code>e</code> parameter of the event. We check to see if the <em>left</em> mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a <code>QDrag</code> object, passing in <code>self</code> to give us access later to the widget that was dragged. We also <em>must</em> pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.</p>
<p>Finally, we initiate a drag by calling <code>drag.exec_(Qt.MoveAction)</code>. As with dialogs <code>exec_()</code> starts a new event loop, blocking the main loop until the drag is complete. The parameter <code>Qt.MoveAction</code> tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.</p>
<p>You can update the main window code to use our new <code>DragButton</code> class as follows.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run the code now, you <em>can</em> drag the buttons, but you'll notice the drag is forbidden.</p>
<p><img alt="Drag forbidden" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-forbidden.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts but is forbidden.</em></p>
<p>What's happening? The mouse movement is being detected by our <code>DragButton</code> object and the drag started, but the main window does not accept drag & drop.</p>
<p>To fix this we need to enable drops on the window and implement <code>dragEnterEvent</code> to actually accept them.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling <code>drag.exec_()</code>.</p>
<p><img alt="Drag accepted" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-accepted.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts and is accepted, showing a move icon.</em></p>
<p>Releasing the mouse button during a drag drop operation triggers a <code>dropEvent</code> on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our <code>dropEvent</code> method.</p>
<p>The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.</p>
<p>To determine where to place the widget, we iterate over all the widgets in the layout, <em>until</em> we find one who's <code>x</code> position is <em>greater</em> than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.</p>
<p>If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment <code>n</code> one further (in the <code>else:</code> block below).</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x():
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using <code>if pos.x() < w.x() + w.size().width() // 2:</code> -- that is x + half of the width.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The complete working drag-drop code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<h2 id="visual-drag-drop">Visual Drag & Drop</h2>
<p>We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.</p>
<p>Qt's <code>QDrag</code> handler natively provides a mechanism for showing dragged objects which we can use. We can update our <code>DragButton</code> class to pass a <em>pixmap</em> image to <code>QDrag</code> and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a <code>QPixmap</code> of the widget we're dragging.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>To create the pixmap we create a <code>QPixmap</code> object passing in the size of the widget this event is fired on with <code>self.size()</code>. This creates an empty <em>pixmap</em> which we can then pass into <code>self.render</code> to <em>render</em> -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the <code>drag</code> object.</p>
<p>If you run the code with this modification you'll see something like the following --</p>
<p><img alt="Drag visual" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-visual.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget showing the dragged widget.</em></p>
<h2 id="generic-drag-drop-container">Generic Drag & Drop Container</h2>
<p>We now have a working drag and drop behavior implemented on our window.
We can take this a step further and implement a <em>generic</em> drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget <code>DragWidget</code> which can be added to any window.</p>
<p>You can add <em>items</em> -- instances of <code>DragItem</code> -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal <code>orderChanged</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
break
else:
# We aren't on the left hand/upper side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
self.orderChanged.emit(self.get_item_data())
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p><img alt="Generic drag drop horizontal" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-horizontal.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-600 600w" loading="lazy" width="306" height="139"/>
<em>Generic drag-drop sorting in horizontal orientation.</em></p>
<p>You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal <code>QLabel</code> which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call <code>get_item_data</code> yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort <em>anything</em> not just strings.</p>
<p>In the example above we're passing in the enumerated index as the data, so dragging will output (via the <code>print</code> connected to <code>orderChanged</code>) something like:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]
</code></pre>
</div>
<p>If you remove the <code>item.set_data(n)</code> you'll see the labels emitted on changes.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']
</code></pre>
</div>
<p>We've also implemented <em>orientation</em> onto the <code>DragWidget</code> using the Qt built in flags <code>Qt.Orientation.Vertical</code> or <code>Qt.Orientation.Horizontal</code>. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.</p>
<p><img alt="Generic drag drop vertical" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-vertical.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-600 600w" loading="lazy" width="202" height="216"/>
<em>Generic drag-drop sorting in vertical orientation.</em></p>
<h2 id="adding-a-visual-drop-target">Adding a Visual Drop Target</h2>
<p>If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be
inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using <em>guesswork</em> to get it right.</p>
<p>With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.</p>
<p>In this final section we'll implement this type of drag and drop preview indicator.</p>
<p>The first step is to define our target indicator. This is just another label,
which in our example is empty, with custom styles applied to make it have a
solid "shadow" like background. This makes it obviously different to the
items in the list, so it stands out as something distinct.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.</p>
<p>The drag item is unchanged, but we need to implement some additional behavior on our <code>DragWidget</code> to add the target, control showing and moving it.</p>
<p>First we'll add the drag target indicator to the layout on our <code>DragWidget</code>. This is hidden to begin with, but will be shown during the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
</code></pre>
</div>
<p>Next we modify the <code>DragWidget.dragMoveEvent</code> to show the drag target indicator. We show it by <em>inserting</em> it into the layout and then calling <code>.show</code> -- inserting a widget which is already in a layout will move it.
We also hide the original item which is being dragged.</p>
<p>In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.</p>
<p>Instead, the dragged item is left in place and hidden during move.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
</code></pre>
</div>
<p>The method <code>self._find_drop_location</code> finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.</p>
<p>The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
</code></pre>
</div>
<p>The drop location <code>n</code> is returned for use in the <code>dragMoveEvent</code> to place the drop target indicator.</p>
<p>Next wee need to update the <code>get_item_data</code> handler to ignore the drop target indicator. To do this we check <code>w</code> against <code>self._drag_target_indicator</code> and skip if it is the same. With this change the method will work as expected.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
</code></pre>
</div>
<p>If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).</p>
<p>To fix that we need to implement a <code>dragLeaveEvent</code> which hides the indicator.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
</code></pre>
</div>
<p>With those changes, the drag-drop behavior should be working as intended.
The complete code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
def dropEvent(self, e):
widget = e.source()
# Use drop target location for destination, then remove it.
self._drag_target_indicator.hide()
index = self.blayout.indexOf(self._drag_target_indicator)
if index is not None:
self.blayout.insertWidget(index, widget)
self.orderChanged.emit(self.get_item_data())
widget.show()
self.blayout.activate()
e.accept()
def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this example on macOS you may notice that the widget drag preview (the <code>QPixmap</code> created on <code>DragItem</code>) is a bit blurry. On high-resolution screens you need to set the <em>device pixel ratio</em> and scale up the pixmap when
you create it. Below is a modified <code>DragItem</code> class which does this.</p>
<p>Update <code>DragItem</code> to support high resolution screens.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
# Render at x2 pixel ratio to avoid blur on Retina screens.
pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
pixmap.setDevicePixelRatio(2)
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
</code></pre>
</div>
<p>That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>Working With Python Virtual Environments — Setting Your Python Working Environment, the Right Way2024-03-04T06:00:00+00:002024-03-04T06:00:00+00:00Punition Chaetognathantag:www.pythonguis.com,2024-03-04:/tutorials/python-virtual-environments/<p>As Python developers, we often need to add functionality to our applications which isn't provided by the standard library. Rather than implement everything ourselves, we can instead install 3rd party Python packages from the official Python package index at <a href="https://pypi.org/">PyPI</a> using <a href="https://pip.pypa.io/en/stable/"><code>pip</code></a>. The <code>pip</code> tool downloads and installs these 3rd party packages into our Python installation so we can immediately use them in our scripts and applications.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> The <a href="https://docs.python.org/3/library/index.html">Python standard library</a> is a set of Python packages that come bundled with Python installers. This library includes modules and packages like <a href="https://docs.python.org/3/library/math.html#module-math"><code>math</code></a> and usually <a href="https://www.pythonguis.com/tkinter/">Tkinter</a>.</p>
<p>There is a caveat here though: we can only install a single version of a package at any one time. While this isn't a problem when you create your first project, when you create the second any changes you make to the dependencies will also affect the first project. If any of your dependencies depend on other packages themselves (usually the case!) you may encounter conflicts where fixing the dependencies for one project breaks the dependencies for another.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> This is known as <em>dependency hell</em>.</p>
<p>Thankfully, Python has a solution for this: <strong>Python virtual environments</strong>. Using virtual environments you can manage the packages for each project independently.</p>
<p>In this tutorial, we will learn how to create virtual environments and use them to manage our Python projects and their dependencies. We will also learn why virtual environments are an essential tool in any Python developer's arsenal.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#understand-why-we-need-python-virtual-environments">Understand Why We Need Python Virtual Environments</a></li>
<li><a href="#explore-how-python-virtual-environments-work">Explore How Python Virtual Environments Work</a></li>
<li><a href="#install-python-on-your-system">Install Python On Your System</a><ul>
<li><a href="#install-python-on-windows-or-macos">Install Python on Windows or macOS</a></li>
<li><a href="#install-python-on-linux">Install Python on Linux</a></li>
</ul>
</li>
<li><a href="#create-python-virtual-environments">Create Python Virtual Environments</a></li>
<li><a href="#use-python-virtual-environments">Use Python Virtual Environments</a><ul>
<li><a href="#activate-the-virtual-environment">Activate the Virtual Environment</a></li>
<li><a href="#install-packages-in-the-virtual-environment">Install Packages in the Virtual Environment</a></li>
<li><a href="#deactivate-a-virtual-environment">Deactivate a Virtual Environment</a></li>
<li><a href="#manage-the-location-of-your-virtual-environments">Manage the Location of Your Virtual Environments</a></li>
<li><a href="#delete-a-python-virtual-environments">Delete a Python Virtual Environments</a></li>
</ul>
</li>
<li><a href="#manage-your-projects-dependencies-with-pip">Manage Your Project's Dependencies With pip</a><ul>
<li><a href="#generate-a-requirement-file">Generate a Requirement File</a></li>
<li><a href="#install-project-dependencies-from-a-requirement-file">Install Project Dependencies From a Requirement File</a></li>
<li><a href="#tweak-the-requirements-file">Tweak the Requirements File</a></li>
<li><a href="#create-a-development-requirement-file">Create a Development Requirement File</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="understand-why-we-need-python-virtual-environments">Understand Why We Need Python Virtual Environments</h2>
<p>When working on a project, we often rely on specific external or third-party Python package versions. However, as packages get updated, their way of working may change.</p>
<p>If we update a package, the changes in that package may mean other projects stop functioning. To solve this issue, we have a few different options:</p>
<ul>
<li>Continue using the <strong>outdated version</strong> of the package in all our projects.</li>
<li>Update <em>all</em> our projects whenever a <strong>new version</strong> of the package comes out.</li>
<li>Use Python virtual environments to <strong>isolate</strong> the projects and their dependencies.</li>
</ul>
<p>Not updating packages means that we won't be able to take advantage of new features or improvements. It can also affect the security of our project because we're not up-to-date with essential security updates and bug fixes for the Python packages our project relies on.</p>
<p>On the other hand, constantly updating our projects to keep them functional isn't much fun. While not always necessary, it quickly can become tedious and impractical, depending on the scale and the number of projects we have.</p>
<p>The last and best option is to use Python virtual environments. They allow us to manage the Python packages needed for each application separately. So we can choose when to update dependencies without affecting other projects that rely on those packages.</p>
<p>Using virtual environments gives us the benefit of being able to update packages without the risk of breaking all our projects at once. It gives us more time to make sure that each project works properly with the updated package version. It also avoids conflicts between package requirements and dependency requirements in our various projects.</p>
<p>Finally, using Python virtual environments is also recommended to avoid system conflicts because updating a package may break essential system tools or libraries that rely on a particular version of the package.</p>
<h2 id="explore-how-python-virtual-environments-work">Explore How Python Virtual Environments Work</h2>
<p>A <strong>virtual environment</strong> is a folder containing a lightweight installation of Python. It creates a stripped-down and isolated copy of the base Python installation on our system without requiring us to install Python again.</p>
<p>Because a virtual environment is an isolated copy of our current system Python, we will find a copy of the <code>python</code> and <code>pip</code> executables inside each virtual environment folder. Once we activate a virtual environment, any <code>python</code> or <code>pip</code> commands will point to these executables instead of the ones in our system installation.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> We can check where our system currently points Python commands to by running <code>python -c "import sys; print(sys.executable)"</code>. If we are not using a virtual environment, this command will show us where the system Python installation is located. Inside a virtual environment it will point to the environment's executable.</p>
<p>Using a virtual environment also means that <code>pip</code> will install external packages in the environment's <code>site</code> folder rather than in the system's. This way, we can keep different versions of a Python package installed in independent virtual environments. However, we still can only have one version of a given package per virtual environment.</p>
<h2 id="install-python-on-your-system">Install Python On Your System</h2>
<p>In case you haven't done it yet, you need to install Python on your development machine before being able to use it.</p>
<h3 id="install-python-on-windows-or-macos">Install Python on Windows or macOS</h3>
<p>You can install Python by going to its <a href="https://www.python.org/downloads/">download page</a> and grabbing the specific installer for either Windows or macOS. Then, you have to run the installer and follow the on-screen instructions. Make sure you select the option to <em>Add Python to PATH</em> during the installation process.</p>
<p>Python is also available for installation through <a href="https://apps.microsoft.com/store/search/python">Microsoft Store</a> on Windows machines.</p>
<h3 id="install-python-on-linux">Install Python on Linux</h3>
<p>If you are on Linux, you can check if Python is installed on your machine by running the <code>python3 --version</code> command in a terminal. If this command issues an error, you can install Python from your Linux distribution repository.</p>
<p class="admonition admonition-warning"><span class="admonition-kind"><i class="fas fa-exclamation-circle"></i></span> You will find that the Python version available in most Linux repositories is relatively old. To work around this issue, you can use tools like <a href="https://github.com/pyenv/pyenv">pyenv</a>, which allows the installation of multiple Python versions.</p>
<p>For example, on Ubuntu and Debian, you can install Python by executing:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ sudo apt install python3
</code></pre>
</div>
<p>You may also need to install <a href="https://pip.pypa.io/en/stable/"><code>pip</code></a>, the Python package installer, and <a href="https://docs.python.org/3/library/venv.html#module-venv"><code>venv</code></a>, the module that allows you to create virtual environments. To do this, run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ sudo apt install python3-pip python3-venv
</code></pre>
</div>
<p>You only need to install <code>pip</code> and <code>venv</code> separately in some Linux distributions, including Ubuntu and Debian.</p>
<h2 id="create-python-virtual-environments">Create Python Virtual Environments</h2>
<p>The standard way to create virtual environments in Python is to use the <code>venv</code> module, which is a part of the standard library, so you shouldn't need to install anything additional on most systems.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> A virtual environment is a stripped-down and isolated copy of an existing Python installation, so it doesn't require downloading anything.</p>
<p>You'll typically create a virtual environment per project. However, you can also have custom virtual environments with different purposes in your system.</p>
<p>To add a new virtual environment to a project, go to your project folder and run the following command in a terminal:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m venv ./venv
</code></pre>
</div>
<p>If you check inside your project folder now, you'll see a new subfolder named <code>venv</code>. This folder contains the virtual environment you just made.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> Using <code>venv</code>, <code>env</code>, or <code>.venv</code> as the virtual environment name is a common and accepted practice in the Python community.</p>
<h2 id="use-python-virtual-environments">Use Python Virtual Environments</h2>
<p>Now that you've successfully created your Python virtual environment, you can start using it to install whatever packages you need for your project. Note that every new virtual is like a fresh Python installation, with no third-party package available.</p>
<p>Unless you choose to pass the <code>--system-site-packages</code> switch to the <code>venv</code> command when you create the virtual environment, it will only contain the Python standard library and a couple of required packages. For any additional packages, we need to use <code>pip</code> to install them.</p>
<p>In the following sections, you'll learn how to activate your virtual environment for use, install packages, manage dependency, and more.</p>
<h3 id="activate-the-virtual-environment">Activate the Virtual Environment</h3>
<p>To start using a virtual environment, you need to <em>activate</em> it. The command to do this depends on your operating system and terminal shell.</p>
<p>On Unix systems, such as macOS and Linux, you can run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ source venv/bin/activate
</code></pre>
</div>
<p>This command activates your virtual environment, making it ready for use. You'll know that because your prompt will change from <code>$</code> to <code>(venv) $</code>. Go ahead and run the following command to check your current Python interpreter:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -c "import sys; print(sys.executable)"
/path/to/project/venv/bin/python
</code></pre>
</div>
<p>The output of this command will contain the path to the virtual environment interpreter. This interpreter is different from your system interpreter.</p>
<p>If you're on Windows and using PowerShell, then you can activate your virtual environment by running the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">PS> venv\Scripts\activate
</code></pre>
</div>
<p>Again, you'll know the environment is active because your prompt will change from <code>PS></code> to <code>(venv) PS></code>.</p>
<p>Great! With these steps completed, we're now ready to start using our Python virtual environment.</p>
<h3 id="install-packages-in-the-virtual-environment">Install Packages in the Virtual Environment</h3>
<p>Once the virtual environment is active, we can start using <code>pip</code> to install any packages our project requires. For example, say you want to install the <a href="https://www.pythonguis.com/pyqt6/">PyQt</a> GUI framework to create desktop apps. In this case, you can run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install pyqt6
</code></pre>
</div>
<p>This command downloads and installs PyQt from the Python package index directly. Now you can start working on your GUI project.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> Once you've activated a virtual environment, you can use <code>pip</code> directly without the <code>python -m</code> prefix. However, <a href="https://snarky.ca/why-you-should-use-python-m-pip/">best practices</a> recommend using this command format.</p>
<p>You can also install multiple packages in one go:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install black flake8 mypy pytest
</code></pre>
</div>
<p>To install multiple packages with a single command, you only have to list the desired packages separated by spaces, as you did in the above command.</p>
<p>Finally, sometimes it's also useful to update a given package to its latest version:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install --upgrade pyqt6
</code></pre>
</div>
<p>The <code>--upgrade</code> flag tells <code>pip</code> to install the latest available version of an already installed package.</p>
<h3 id="deactivate-a-virtual-environment">Deactivate a Virtual Environment</h3>
<p>Once you are done working on your GUI app, you need to <em>deactivate</em> the virtual environment so you can switch back to your system shell or terminal. Remember that you'll have to reactivate this virtual environment next time you need to work on your project.</p>
<p>Here's how you can deactivate a Python virtual environment:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ deactivate
</code></pre>
</div>
<p>This command deactivates your virtual environment and gets you back into your system shell. You can run the <code>python -c "import sys; print(sys.executable)"</code> command to check that your current Python interpreter is your system Python.</p>
<h3 id="manage-the-location-of-your-virtual-environments">Manage the Location of Your Virtual Environments</h3>
<p>Although you can place your virtual environments anywhere on your computer, there are a few standardized places for them to live. The most common one is inside the relevant project folder. As you already learned, the folder will usually be named <code>venv</code> or <code>.venv</code>.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If you use <a href="https://www.pythonguis.com/tutorials/git-github-python/">Git for version control</a>, make sure to ignore the virtual environment folder by adding it to <code>.gitignore</code>.</p>
<p>If you want to have all your virtual environments in a central location, then you can place them in your home folder. Directories like <code>~/.venvs</code> and <code>~/.virtualvenvs</code> are commonly used locations.</p>
<p>The Python extension for <a href="https://www.pythonguis.com/tutorials/getting-started-vs-code-python/">Visual Studio Code</a> automatically <a href="https://code.visualstudio.com/docs/python/environments#_where-the-extension-looks-for-environments">looks for any virtual environments</a> in a couple of places, including inside your current project folder and specific folders in your home folder (if they exist). If you want to specify additional external folders for it to look in, go to <em>Settings</em> and configure <code>python.venvFolders</code> or <code>python.venvPath</code>. To avoid confusion, a good idea is to name each virtual environment in this folder after the relevant project name.</p>
<h3 id="delete-a-python-virtual-environments">Delete a Python Virtual Environments</h3>
<p>If you are no longer working with a given virtual environment, you can get rid of it and all the related Python packages by simply deleting the virtual environment folder.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> If you delete the virtual environment associated with a given project, to run the project again, you'll have to create a new virtual environment and install the required packages.</p>
<h2 id="manage-your-projects-dependencies-with-pip">Manage Your Project's Dependencies With <code>pip</code></h2>
<p>You've already learned how to create Python virtual environments to isolate the dependencies of your projects and avoid package versioning issues across different projects. You also learned how to install external dependencies with <code>pip</code>.</p>
<p>In the following sections, you'll learn how to efficiently manage a project's dependencies using <code>pip</code> and requirement files.</p>
<h3 id="generate-a-requirement-file">Generate a Requirement File</h3>
<p>An important advantage of using a dedicated virtual environment for each of our Python projects is that the environment will only contain the packages for that specific project. We can use the <code>pip freeze</code> command to get the list of currently installed packages in any active virtual environment:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip freeze
PyQt6==6.5.0
PyQt6-Qt6==6.5.0
PyQt6-sip==13.5.1
</code></pre>
</div>
<p>This command allows us to create a text file containing the lists of the packages our project needs for running. To do this, go ahead and run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip freeze > requirements.txt
</code></pre>
</div>
<p>After running the above command, you'll have a new file called <code>requirements.txt</code> in your project's directory.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> By convention, the Python community uses the name <code>requirements.txt</code> for those files containing the list of dependencies of a given project. This file typically lives in the project's root directory.</p>
<p>Run the following command to check the content of this new file:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements.txt
PyQt6==6.5.0
PyQt6-Qt6==6.5.0
PyQt6-sip==13.5.1
</code></pre>
</div>
<p>As you can see, your <code>requirements.txt</code> file lists those packages that you've installed in the active virtual environment. Once you have this file in place, anyone who needs to run your project from its source will know which packages they need to install. More importantly, they can use the file to install all the dependencies automatically, as you'll learn in the following section.</p>
<h3 id="install-project-dependencies-from-a-requirement-file">Install Project Dependencies From a Requirement File</h3>
<p>Besides of being able to see the list of Python packages our application needs, the requirements file also makes installing those packages in a new virtual environment just one command away:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(new_venv) $ python -m pip install -r requirements.txt
</code></pre>
</div>
<p>The <code>-r</code> option of <code>pip install</code> allows you to provide a requirement file as an argument. Then, <code>pip</code> will read that file and install all the packages listed in it. If you run <code>pip freeze</code> again, you'll note that this new environment has the same packages installed.</p>
<h3 id="tweak-the-requirements-file">Tweak the Requirements File</h3>
<p>Although using <code>pip freeze</code> is quite convenient, it often creates a lot of unnecessary clutter by populating the requirement file with Python packages that our application may not rely on directly. For example, packages that are dependencies of required packages and also packages that are only needed for development.</p>
<p>The generated file will also include the exact version of each package we have installed. While this may be useful, it is best to keep the requirement file clean. For example, dependencies of dependencies can often be ignored, since managing those is the responsibility of that package. That way, it'll be easy to know which packages are really required.</p>
<p>For example, in a GUI project, the <code>requirements.txt</code> file may only need to include PyQt6. In that case it would look like this:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements.txt
PyQt6==6.5.0
</code></pre>
</div>
<p>Specifying the highest or lowest version of required packages may also be beneficial. To do this, we can replace the equality operator (<code>==</code>) with the less than (<code><=</code>) or greater than (<code>>=</code>) operators, depending on your needs. If we completely omit the package version, <code>pip</code> will install the latest version available.</p>
<h3 id="create-a-development-requirement-file">Create a Development Requirement File</h3>
<p>We should consider splitting our project requirement file into two separate files. For example, <code>requirements.txt</code> and <code>requirements_dev.txt</code>. This separation lets other developers know which packages are required for your project and which are solely relevant for development and testing purposes.</p>
<p>For example, you may use Black for formatting, <code>flake8</code> for linting, <code>mypy</code> for type checking, and <code>pytest</code> for testing your code. In this case, your <code>requirements_dev.txt</code> file should look something like this:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements_dev.txt
black
flake8
mypy
pytest
</code></pre>
</div>
<p>With this file in place, developers who want to contribute to your project can install the required development dependencies easily.</p>
<h2 id="conclusion">Conclusion</h2>
<p>By now, you have a good understanding of how Python virtual environments work. They are straightforward to create and make it easy to manage the Python packages you need for developing your applications and projects.</p>
<p>Avoid installing Python packages outside of a virtual environment whenever possible. Creating a dedicated environment for a small Python script may not make sense. However, it's always a good idea to start any Python project that requires external packages by creating its own virtual environment.</p><p>As Python developers, we often need to add functionality to our applications which isn't provided by the standard library. Rather than implement everything ourselves, we can instead install 3rd party Python packages from the official Python package index at <a href="https://pypi.org/">PyPI</a> using <a href="https://pip.pypa.io/en/stable/"><code>pip</code></a>. The <code>pip</code> tool downloads and installs these 3rd party packages into our Python installation so we can immediately use them in our scripts and applications.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> The <a href="https://docs.python.org/3/library/index.html">Python standard library</a> is a set of Python packages that come bundled with Python installers. This library includes modules and packages like <a href="https://docs.python.org/3/library/math.html#module-math"><code>math</code></a> and usually <a href="https://www.pythonguis.com/tkinter/">Tkinter</a>.</p>
<p>There is a caveat here though: we can only install a single version of a package at any one time. While this isn't a problem when you create your first project, when you create the second any changes you make to the dependencies will also affect the first project. If any of your dependencies depend on other packages themselves (usually the case!) you may encounter conflicts where fixing the dependencies for one project breaks the dependencies for another.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> This is known as <em>dependency hell</em>.</p>
<p>Thankfully, Python has a solution for this: <strong>Python virtual environments</strong>. Using virtual environments you can manage the packages for each project independently.</p>
<p>In this tutorial, we will learn how to create virtual environments and use them to manage our Python projects and their dependencies. We will also learn why virtual environments are an essential tool in any Python developer's arsenal.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#understand-why-we-need-python-virtual-environments">Understand Why We Need Python Virtual Environments</a></li>
<li><a href="#explore-how-python-virtual-environments-work">Explore How Python Virtual Environments Work</a></li>
<li><a href="#install-python-on-your-system">Install Python On Your System</a><ul>
<li><a href="#install-python-on-windows-or-macos">Install Python on Windows or macOS</a></li>
<li><a href="#install-python-on-linux">Install Python on Linux</a></li>
</ul>
</li>
<li><a href="#create-python-virtual-environments">Create Python Virtual Environments</a></li>
<li><a href="#use-python-virtual-environments">Use Python Virtual Environments</a><ul>
<li><a href="#activate-the-virtual-environment">Activate the Virtual Environment</a></li>
<li><a href="#install-packages-in-the-virtual-environment">Install Packages in the Virtual Environment</a></li>
<li><a href="#deactivate-a-virtual-environment">Deactivate a Virtual Environment</a></li>
<li><a href="#manage-the-location-of-your-virtual-environments">Manage the Location of Your Virtual Environments</a></li>
<li><a href="#delete-a-python-virtual-environments">Delete a Python Virtual Environments</a></li>
</ul>
</li>
<li><a href="#manage-your-projects-dependencies-with-pip">Manage Your Project's Dependencies With pip</a><ul>
<li><a href="#generate-a-requirement-file">Generate a Requirement File</a></li>
<li><a href="#install-project-dependencies-from-a-requirement-file">Install Project Dependencies From a Requirement File</a></li>
<li><a href="#tweak-the-requirements-file">Tweak the Requirements File</a></li>
<li><a href="#create-a-development-requirement-file">Create a Development Requirement File</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="understand-why-we-need-python-virtual-environments">Understand Why We Need Python Virtual Environments</h2>
<p>When working on a project, we often rely on specific external or third-party Python package versions. However, as packages get updated, their way of working may change.</p>
<p>If we update a package, the changes in that package may mean other projects stop functioning. To solve this issue, we have a few different options:</p>
<ul>
<li>Continue using the <strong>outdated version</strong> of the package in all our projects.</li>
<li>Update <em>all</em> our projects whenever a <strong>new version</strong> of the package comes out.</li>
<li>Use Python virtual environments to <strong>isolate</strong> the projects and their dependencies.</li>
</ul>
<p>Not updating packages means that we won't be able to take advantage of new features or improvements. It can also affect the security of our project because we're not up-to-date with essential security updates and bug fixes for the Python packages our project relies on.</p>
<p>On the other hand, constantly updating our projects to keep them functional isn't much fun. While not always necessary, it quickly can become tedious and impractical, depending on the scale and the number of projects we have.</p>
<p>The last and best option is to use Python virtual environments. They allow us to manage the Python packages needed for each application separately. So we can choose when to update dependencies without affecting other projects that rely on those packages.</p>
<p>Using virtual environments gives us the benefit of being able to update packages without the risk of breaking all our projects at once. It gives us more time to make sure that each project works properly with the updated package version. It also avoids conflicts between package requirements and dependency requirements in our various projects.</p>
<p>Finally, using Python virtual environments is also recommended to avoid system conflicts because updating a package may break essential system tools or libraries that rely on a particular version of the package.</p>
<h2 id="explore-how-python-virtual-environments-work">Explore How Python Virtual Environments Work</h2>
<p>A <strong>virtual environment</strong> is a folder containing a lightweight installation of Python. It creates a stripped-down and isolated copy of the base Python installation on our system without requiring us to install Python again.</p>
<p>Because a virtual environment is an isolated copy of our current system Python, we will find a copy of the <code>python</code> and <code>pip</code> executables inside each virtual environment folder. Once we activate a virtual environment, any <code>python</code> or <code>pip</code> commands will point to these executables instead of the ones in our system installation.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> We can check where our system currently points Python commands to by running <code>python -c "import sys; print(sys.executable)"</code>. If we are not using a virtual environment, this command will show us where the system Python installation is located. Inside a virtual environment it will point to the environment's executable.</p>
<p>Using a virtual environment also means that <code>pip</code> will install external packages in the environment's <code>site</code> folder rather than in the system's. This way, we can keep different versions of a Python package installed in independent virtual environments. However, we still can only have one version of a given package per virtual environment.</p>
<h2 id="install-python-on-your-system">Install Python On Your System</h2>
<p>In case you haven't done it yet, you need to install Python on your development machine before being able to use it.</p>
<h3 id="install-python-on-windows-or-macos">Install Python on Windows or macOS</h3>
<p>You can install Python by going to its <a href="https://www.python.org/downloads/">download page</a> and grabbing the specific installer for either Windows or macOS. Then, you have to run the installer and follow the on-screen instructions. Make sure you select the option to <em>Add Python to PATH</em> during the installation process.</p>
<p>Python is also available for installation through <a href="https://apps.microsoft.com/store/search/python">Microsoft Store</a> on Windows machines.</p>
<h3 id="install-python-on-linux">Install Python on Linux</h3>
<p>If you are on Linux, you can check if Python is installed on your machine by running the <code>python3 --version</code> command in a terminal. If this command issues an error, you can install Python from your Linux distribution repository.</p>
<p class="admonition admonition-warning"><span class="admonition-kind"><i class="fas fa-exclamation-circle"></i></span> You will find that the Python version available in most Linux repositories is relatively old. To work around this issue, you can use tools like <a href="https://github.com/pyenv/pyenv">pyenv</a>, which allows the installation of multiple Python versions.</p>
<p>For example, on Ubuntu and Debian, you can install Python by executing:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ sudo apt install python3
</code></pre>
</div>
<p>You may also need to install <a href="https://pip.pypa.io/en/stable/"><code>pip</code></a>, the Python package installer, and <a href="https://docs.python.org/3/library/venv.html#module-venv"><code>venv</code></a>, the module that allows you to create virtual environments. To do this, run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ sudo apt install python3-pip python3-venv
</code></pre>
</div>
<p>You only need to install <code>pip</code> and <code>venv</code> separately in some Linux distributions, including Ubuntu and Debian.</p>
<h2 id="create-python-virtual-environments">Create Python Virtual Environments</h2>
<p>The standard way to create virtual environments in Python is to use the <code>venv</code> module, which is a part of the standard library, so you shouldn't need to install anything additional on most systems.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> A virtual environment is a stripped-down and isolated copy of an existing Python installation, so it doesn't require downloading anything.</p>
<p>You'll typically create a virtual environment per project. However, you can also have custom virtual environments with different purposes in your system.</p>
<p>To add a new virtual environment to a project, go to your project folder and run the following command in a terminal:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m venv ./venv
</code></pre>
</div>
<p>If you check inside your project folder now, you'll see a new subfolder named <code>venv</code>. This folder contains the virtual environment you just made.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> Using <code>venv</code>, <code>env</code>, or <code>.venv</code> as the virtual environment name is a common and accepted practice in the Python community.</p>
<h2 id="use-python-virtual-environments">Use Python Virtual Environments</h2>
<p>Now that you've successfully created your Python virtual environment, you can start using it to install whatever packages you need for your project. Note that every new virtual is like a fresh Python installation, with no third-party package available.</p>
<p>Unless you choose to pass the <code>--system-site-packages</code> switch to the <code>venv</code> command when you create the virtual environment, it will only contain the Python standard library and a couple of required packages. For any additional packages, we need to use <code>pip</code> to install them.</p>
<p>In the following sections, you'll learn how to activate your virtual environment for use, install packages, manage dependency, and more.</p>
<h3 id="activate-the-virtual-environment">Activate the Virtual Environment</h3>
<p>To start using a virtual environment, you need to <em>activate</em> it. The command to do this depends on your operating system and terminal shell.</p>
<p>On Unix systems, such as macOS and Linux, you can run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ source venv/bin/activate
</code></pre>
</div>
<p>This command activates your virtual environment, making it ready for use. You'll know that because your prompt will change from <code>$</code> to <code>(venv) $</code>. Go ahead and run the following command to check your current Python interpreter:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -c "import sys; print(sys.executable)"
/path/to/project/venv/bin/python
</code></pre>
</div>
<p>The output of this command will contain the path to the virtual environment interpreter. This interpreter is different from your system interpreter.</p>
<p>If you're on Windows and using PowerShell, then you can activate your virtual environment by running the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">PS> venv\Scripts\activate
</code></pre>
</div>
<p>Again, you'll know the environment is active because your prompt will change from <code>PS></code> to <code>(venv) PS></code>.</p>
<p>Great! With these steps completed, we're now ready to start using our Python virtual environment.</p>
<h3 id="install-packages-in-the-virtual-environment">Install Packages in the Virtual Environment</h3>
<p>Once the virtual environment is active, we can start using <code>pip</code> to install any packages our project requires. For example, say you want to install the <a href="https://www.pythonguis.com/pyqt6/">PyQt</a> GUI framework to create desktop apps. In this case, you can run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install pyqt6
</code></pre>
</div>
<p>This command downloads and installs PyQt from the Python package index directly. Now you can start working on your GUI project.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> Once you've activated a virtual environment, you can use <code>pip</code> directly without the <code>python -m</code> prefix. However, <a href="https://snarky.ca/why-you-should-use-python-m-pip/">best practices</a> recommend using this command format.</p>
<p>You can also install multiple packages in one go:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install black flake8 mypy pytest
</code></pre>
</div>
<p>To install multiple packages with a single command, you only have to list the desired packages separated by spaces, as you did in the above command.</p>
<p>Finally, sometimes it's also useful to update a given package to its latest version:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip install --upgrade pyqt6
</code></pre>
</div>
<p>The <code>--upgrade</code> flag tells <code>pip</code> to install the latest available version of an already installed package.</p>
<h3 id="deactivate-a-virtual-environment">Deactivate a Virtual Environment</h3>
<p>Once you are done working on your GUI app, you need to <em>deactivate</em> the virtual environment so you can switch back to your system shell or terminal. Remember that you'll have to reactivate this virtual environment next time you need to work on your project.</p>
<p>Here's how you can deactivate a Python virtual environment:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ deactivate
</code></pre>
</div>
<p>This command deactivates your virtual environment and gets you back into your system shell. You can run the <code>python -c "import sys; print(sys.executable)"</code> command to check that your current Python interpreter is your system Python.</p>
<h3 id="manage-the-location-of-your-virtual-environments">Manage the Location of Your Virtual Environments</h3>
<p>Although you can place your virtual environments anywhere on your computer, there are a few standardized places for them to live. The most common one is inside the relevant project folder. As you already learned, the folder will usually be named <code>venv</code> or <code>.venv</code>.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If you use <a href="https://www.pythonguis.com/tutorials/git-github-python/">Git for version control</a>, make sure to ignore the virtual environment folder by adding it to <code>.gitignore</code>.</p>
<p>If you want to have all your virtual environments in a central location, then you can place them in your home folder. Directories like <code>~/.venvs</code> and <code>~/.virtualvenvs</code> are commonly used locations.</p>
<p>The Python extension for <a href="https://www.pythonguis.com/tutorials/getting-started-vs-code-python/">Visual Studio Code</a> automatically <a href="https://code.visualstudio.com/docs/python/environments#_where-the-extension-looks-for-environments">looks for any virtual environments</a> in a couple of places, including inside your current project folder and specific folders in your home folder (if they exist). If you want to specify additional external folders for it to look in, go to <em>Settings</em> and configure <code>python.venvFolders</code> or <code>python.venvPath</code>. To avoid confusion, a good idea is to name each virtual environment in this folder after the relevant project name.</p>
<h3 id="delete-a-python-virtual-environments">Delete a Python Virtual Environments</h3>
<p>If you are no longer working with a given virtual environment, you can get rid of it and all the related Python packages by simply deleting the virtual environment folder.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> If you delete the virtual environment associated with a given project, to run the project again, you'll have to create a new virtual environment and install the required packages.</p>
<h2 id="manage-your-projects-dependencies-with-pip">Manage Your Project's Dependencies With <code>pip</code></h2>
<p>You've already learned how to create Python virtual environments to isolate the dependencies of your projects and avoid package versioning issues across different projects. You also learned how to install external dependencies with <code>pip</code>.</p>
<p>In the following sections, you'll learn how to efficiently manage a project's dependencies using <code>pip</code> and requirement files.</p>
<h3 id="generate-a-requirement-file">Generate a Requirement File</h3>
<p>An important advantage of using a dedicated virtual environment for each of our Python projects is that the environment will only contain the packages for that specific project. We can use the <code>pip freeze</code> command to get the list of currently installed packages in any active virtual environment:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip freeze
PyQt6==6.5.0
PyQt6-Qt6==6.5.0
PyQt6-sip==13.5.1
</code></pre>
</div>
<p>This command allows us to create a text file containing the lists of the packages our project needs for running. To do this, go ahead and run the following command:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(venv) $ python -m pip freeze > requirements.txt
</code></pre>
</div>
<p>After running the above command, you'll have a new file called <code>requirements.txt</code> in your project's directory.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> By convention, the Python community uses the name <code>requirements.txt</code> for those files containing the list of dependencies of a given project. This file typically lives in the project's root directory.</p>
<p>Run the following command to check the content of this new file:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements.txt
PyQt6==6.5.0
PyQt6-Qt6==6.5.0
PyQt6-sip==13.5.1
</code></pre>
</div>
<p>As you can see, your <code>requirements.txt</code> file lists those packages that you've installed in the active virtual environment. Once you have this file in place, anyone who needs to run your project from its source will know which packages they need to install. More importantly, they can use the file to install all the dependencies automatically, as you'll learn in the following section.</p>
<h3 id="install-project-dependencies-from-a-requirement-file">Install Project Dependencies From a Requirement File</h3>
<p>Besides of being able to see the list of Python packages our application needs, the requirements file also makes installing those packages in a new virtual environment just one command away:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">(new_venv) $ python -m pip install -r requirements.txt
</code></pre>
</div>
<p>The <code>-r</code> option of <code>pip install</code> allows you to provide a requirement file as an argument. Then, <code>pip</code> will read that file and install all the packages listed in it. If you run <code>pip freeze</code> again, you'll note that this new environment has the same packages installed.</p>
<h3 id="tweak-the-requirements-file">Tweak the Requirements File</h3>
<p>Although using <code>pip freeze</code> is quite convenient, it often creates a lot of unnecessary clutter by populating the requirement file with Python packages that our application may not rely on directly. For example, packages that are dependencies of required packages and also packages that are only needed for development.</p>
<p>The generated file will also include the exact version of each package we have installed. While this may be useful, it is best to keep the requirement file clean. For example, dependencies of dependencies can often be ignored, since managing those is the responsibility of that package. That way, it'll be easy to know which packages are really required.</p>
<p>For example, in a GUI project, the <code>requirements.txt</code> file may only need to include PyQt6. In that case it would look like this:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements.txt
PyQt6==6.5.0
</code></pre>
</div>
<p>Specifying the highest or lowest version of required packages may also be beneficial. To do this, we can replace the equality operator (<code>==</code>) with the less than (<code><=</code>) or greater than (<code>>=</code>) operators, depending on your needs. If we completely omit the package version, <code>pip</code> will install the latest version available.</p>
<h3 id="create-a-development-requirement-file">Create a Development Requirement File</h3>
<p>We should consider splitting our project requirement file into two separate files. For example, <code>requirements.txt</code> and <code>requirements_dev.txt</code>. This separation lets other developers know which packages are required for your project and which are solely relevant for development and testing purposes.</p>
<p>For example, you may use Black for formatting, <code>flake8</code> for linting, <code>mypy</code> for type checking, and <code>pytest</code> for testing your code. In this case, your <code>requirements_dev.txt</code> file should look something like this:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ cat requirements_dev.txt
black
flake8
mypy
pytest
</code></pre>
</div>
<p>With this file in place, developers who want to contribute to your project can install the required development dependencies easily.</p>
<h2 id="conclusion">Conclusion</h2>
<p>By now, you have a good understanding of how Python virtual environments work. They are straightforward to create and make it easy to manage the Python packages you need for developing your applications and projects.</p>
<p>Avoid installing Python packages outside of a virtual environment whenever possible. Creating a dedicated environment for a small Python script may not make sense. However, it's always a good idea to start any Python project that requires external packages by creating its own virtual environment.</p>Which Python GUI library should you use? — Comparing the Python GUI libraries available in 20242024-02-26T06:00:00+00:002024-02-26T06:00:00+00:00Martin Fitzpatricktag:www.pythonguis.com,2024-02-26:/faq/which-python-gui-library/<p>Python is a popular programming used for everything from scripting routine tasks to building websites and performing complex data analysis. While you can accomplish a lot with command line tools, some tasks are better suited to graphical interfaces. You may also find yourself wanting to build a desktop front-end for an existing tool to improve usability for non-technical users. Or maybe you're building some hardware or a mobile app and want an intuitive touchscreen interface.</p>
<p>To create <em>graphical user interfaces</em> with Python, you need a GUI library. Unfortunately, at this point things
get pretty confusing -- there are many different GUI libraries available for Python, all with different capabilities and licensing. <em>Which Python GUI library should you use for your project?</em></p>
<p>In this article, we will look at a selection of the most popular Python GUI frameworks currently available and why you should consider using them for your own projects. You'll learn about the relative strengths of each library, understand the licensing limitations and see a simple <em>Hello, World!</em> application written in each. By the end of the article you should feel confident choosing the right library for your project.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> <strong>tldr</strong> If you're looking to build professional quality software, start with PySide6 or PyQt6. The Qt framework is batteries-included — whatever your project, you'll be able to get it done. We have a complete <a href="https://www.pythonguis.com/pyside6-tutorial">PySide6 tutorial</a> and <a href="https://www.pythonguis.com/pyqt6-tutorial">PyQt6 tutorial</a> as well as a Github respository full of <a href="https://github.com/pythonguis/pythonguis-examples">Python GUI examples</a> to get you started.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#tkinter">Tkinter</a></li>
<li><a href="#pyqt-or-pyside">PyQt or PySide</a></li>
<li><a href="#pyqtpyside-with-qml">PyQt/PySide with QML</a></li>
<li><a href="#kivy">Kivy</a></li>
<li><a href="#pysimplegui">PySimpleGUI</a></li>
<li><a href="#wxpython">WxPython</a></li>
<li><a href="#pygobject-gtk">PyGObject (GTK+)</a></li>
<li><a href="#remi">Remi</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="tkinter">Tkinter</h2>
<p><em><strong>Best for</strong> simple tool GUIs, small portable applications</em></p>
<p>Tkinter is the defacto GUI framework for Python. It comes bundled with Python on both Windows and macOS. (On Linux, it may require downloading an additional package from your distribution's repo.) Tkinter is a wrapper written around the Tk GUI toolkit. Its name is an amalgamation of the words <em>Tk</em> and <em>Interface</em>.</p>
<p>Tkinter is a simple library with support for standard layouts and widgets, as well as more complex widgets such as tabbed views & progressbars. Tkinter is a pure GUI library, not a framework. There is no built-in support for GUIs driven from data sources, databases, or for displaying or manipulating multimedia or hardware. However, if you need to make something simple that doesn't require any additional dependencies, Tkinter may be what you are looking for. Tkinter is cross-platform however the widgets can look outdated, particularly on Windows.</p>
<p><strong>Installation</strong> Already installed with Python on Windows and macOS. Ubuntu/Debian Linux <code>sudo apt install python3-tk</code></p>
<p>A simple <em>hello world</em> application in Tkinter is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="c8714a57212741f2b2e5a4d84e703cdf" v-on:click="switch_tab">Standard</li>
<li class="tab-link" data-tab="c2015f5d4305481d88b8aa94ef2619a8" v-on:click="switch_tab">Class-based</li></ul><div class="tab-content current code-block-outer" id="c8714a57212741f2b2e5a4d84e703cdf">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import tkinter as tk
window = tk.Tk()
window.title("Hello World")
def handle_button_press(event):
window.destroy()
button = tk.Button(text="My simple app.")
button.bind("<button-1>", handle_button_press)
button.pack()
# Start the event loop.
window.mainloop()
</button-1></code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="c2015f5d4305481d88b8aa94ef2619a8">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from tkinter import Tk, Button
class Window(Tk):
def __init__(self):
super().__init__()
self.title("Hello World")
self.button = Button(text="My simple app.")
self.button.bind("<button-1>", self.handle_button_press)
self.button.pack()
def handle_button_press(self, event):
self.destroy()
# Start the event loop.
window = Window()
window.mainloop()
</button-1></code></pre>
</div>
</div>
</div>
<p><img alt="Tkinter Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/tkinter-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-600 600w" loading="lazy" width="387" height="218"/>
<em>Hello world application built using Tkinter, running on Windows 11</em></p>
<p>Tkinter was originally developed by Steen Lumholt and Guido Van Rossum, who designed Python itself. Both the GUI framework and the language are licensed under the same Python Software Foundation (PSF) License. While the license is compatible with the GPL, it is a 'permissive' license (similar to the MIT License) that allows it to be used for proprietary applications and modifications.</p>
<ul>
<li><a href="https://www.pythonguis.com/tkinter/">Tkinter tutorial</a></li>
<li><a href="https://docs.python.org/3/library/tkinter.html">Tkinter Documentation</a></li>
</ul>
<h2 id="pyqt-or-pyside">PyQt or PySide</h2>
<p><em><strong>Best for</strong> commercial, multimedia, scientific or engineering desktop applications</em></p>
<p>PyQt and PySide are wrappers around the Qt framework. They allow you to easily create modern interfaces that look right at home on any platform, including Windows, macOS, Linux and even Android. They also have solid tooling with the most notable being <em>Qt Creator</em>, which includes a WYSIWYG editor for designing GUI interfaces quickly and easily. Being backed by a commercial project means that you will find plenty of support and online learning resources to help you develop your application.</p>
<p>Qt (and by extension PyQt & PySide) is not just a GUI library, but a complete application development framework. In addition to standard UI elements, such as widgets and layouts, Qt provides MVC-like data-driven views (spreadsheets, tables), database interfaces & models, graph plotting, vector graphics visualization, multimedia playback, sound effects & playlists and built-in interfaces for hardware such as printing. The Qt signals and slots models allows large applications to be built from re-usable and isolated components.</p>
<p>While other toolkits can work great when building small & simple tools, Qt really comes into its own for building real <em>commercial-quality</em> applications where you will benefit from the pre-built components. This comes at the expense of a slight learning curve. However, for smaller projects Qt is not really any more complex than other libraries.
Qt Widgets-based applications use platform native widgets to ensure they look and feel at home on Windows, macOS and Qt-based Linux desktops.</p>
<p><strong>Installation</strong> <code>pip install pyqt6</code> or <code>pip install pyside6</code></p>
<p>A simple <em>hello world</em> application in PyQt6, using the Qt Widgets API is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="69d959c84c924e57937cbcb10ee49436" v-on:click="switch_tab">PyQt6</li>
<li class="tab-link" data-tab="a944bf84e8ad4c8fbba07493b46c0377" v-on:click="switch_tab">PySide6</li></ul><div class="tab-content current code-block-outer" id="69d959c84c924e57937cbcb10ee49436">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QMainWindow, QApplication, QPushButton
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
button = QPushButton("My simple app.")
button.pressed.connect(self.close)
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a944bf84e8ad4c8fbba07493b46c0377">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtWidgets import QMainWindow, QApplication, QPushButton
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
button = QPushButton("My simple app.")
button.pressed.connect(self.close)
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
</code></pre>
</div>
</div>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> As you can see, the code is almost identical between PyQt & PySide, so it's not something to be concerned about when you start developing with either: you can always migrate easily if you need to.</p>
<p><img alt="PyQt6 Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pyqt6-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-600 600w" loading="lazy" width="473" height="346"/>
<em>Hello world application built using PyQt6, running on Windows 11</em></p>
<p>Before the Qt Company (under Nokia) released the officially supported PySide library in 2009, Riverbank Computing had released PyQt in 1998. The main difference between these two libraries is in <em>licensing</em>. The free-to-use version of PyQt is licensed under GNU General Public License (GPL) v3 but PySide is licensed under GNU Lesser General Public License (LGPL). This means that PyQt is limited GPL-licensed applications unless you purchase its commercial version, while PySide may be used in non-GPL applications without any additional fee. However, note that both these libraries are separate from Qt itself which also has a free-to-use, open source version and a paid, commercial version.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> For a more information see our article on <a href="https://www.pythonguis.com/faq/pyqt-vs-pyside/">PyQt vs PySide licensing</a>.</p>
<ul>
<li>
<p><strong>PySide6</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyside6-book/">PySide6 Book</a></li>
<li><a href="https://www.pythonguis.com/pyside6-tutorial/">PySide6 Tutorial</a></li>
<li><a href="https://www.qt.io/qt-for-python">PySide Website</a></li>
<li><a href="https://doc.qt.io/qtforpython/">PySide Documentation</a></li>
<li><a href="https://github.com/PySide">GitHub Repository</a></li>
</ul>
</li>
<li>
<p><strong>PyQt6</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyqt6-book/">PyQt6 Book</a></li>
<li><a href="https://www.pythonguis.com/pyqt6-tutorial/">PyQt6 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/software/pyqt/intro">PyQt Website</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/">PyQt6 Documentation</a></li>
</ul>
</li>
<li>
<p><strong>PyQt5</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyqt5-book/">PyQt5 Book</a></li>
<li><a href="https://www.pythonguis.com/pyqt5-tutorial/">PyQt5 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt5/">PyQt6 Documentation</a></li>
</ul>
</li>
</ul>
<h2 id="pyqtpyside-with-qml">PyQt/PySide with QML</h2>
<p><em><strong>Best for</strong> Raspberry Pi, microcontrollers, industrial and consumer electronics</em></p>
<p>When using PyQt and PySide you actually have <em>two</em> options for building your GUIs. We've already introduced the Qt Widgets API
which is well-suited for building desktop applications. But Qt also provides a <em>declarative</em> API in the form of Qt Quick/QML.</p>
<p>Using Qt Quick/QML you have access to the entire Qt framework for building your applications. Your UI consists of two parts: the Python code which
handles the business logic and the QML which defines the structure and behavior of the UI itself. You can control the UI from Python, or use
embedded Javascript code to handle events and animations.</p>
<p>Qt Quick/QML is ideally suited for building modern touchscreen interfaces for microcontrollers or device interfaces -- for example, building
interfaces for microcontrollers like the Raspberry Pi. However you can also use it on desktop to build completely customized application
experiences, like those you find in media player applications like Spotify, or to desktop games.</p>
<p><strong>Installation</strong> <code>pip install pyqt6</code> or <code>pip install pyside6</code></p>
<p>A simple <em>Hello World</em> app in PyQt6 with QML. Save the QML file in the same folder as the Python file, and run as normally.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="7bf228dde9544f2d9be1103bcefcf93b" v-on:click="switch_tab">main.py</li>
<li class="tab-link" data-tab="d2f226d74300490797add2fff8e9032b" v-on:click="switch_tab">main.qml</li></ul><div class="tab-content current code-block-outer" id="7bf228dde9544f2d9be1103bcefcf93b">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('main.qml')
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="d2f226d74300490797add2fff8e9032b">
<div class="code-block">
<span class="code-block-language code-block-qml">qml</span>
<pre><code class="qml">import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 600
height: 500
title: "HelloApp"
Text {
anchors.centerIn: parent
text: "Hello World"
font.pixelSize: 24
}
}
</code></pre>
</div>
</div>
</div>
<p>Licensing for Qt Quick/QML applications is the same as for other PyQt/PySide apps.</p>
<ul>
<li>
<p><strong>PyQt</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/tutorials/qml-qtquick-python-application/">QML/PyQt5 Tutorial</a></li>
<li><a href="https://www.pythonguis.com/tutorials/pyqt6-qml-qtquick-python-application/">QML/PyQt6 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/software/pyqt/intro">PyQt Website</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/">PyQt6 Documentation</a></li>
</ul>
</li>
<li>
<p><strong>PySide</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/tutorials/pyside-qml-qtquick-python-application/">QML/PySide2 Tutorial</a></li>
<li><a href="https://www.pythonguis.com/tutorials/pyside6-qml-qtquick-python-application/">QML/PySide6 Tutorial</a></li>
<li><a href="https://www.qt.io/qt-for-python">PySide Website</a></li>
<li><a href="https://doc.qt.io/qtforpython/">PySide Documentation</a></li>
<li><a href="https://github.com/PySide">GitHub Repository</a></li>
</ul>
</li>
</ul>
<p><img alt="PyQt6 QML Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pyqt6qml-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-600 600w" loading="lazy" width="952" height="861"/>
<em>Hello world application built using PyQt6 & QML, running on Windows 11</em></p>
<h2 id="kivy">Kivy</h2>
<p><em><strong>Best for</strong> Python mobile app development</em></p>
<p>While most other GUI frameworks are <em>bindings</em> to toolkits written in other programming languages, Kivy is perhaps the only framework which is primarily written in pure Python. If you want to create touchscreen-oriented interfaces with a focus on mobile platforms such as Android and iOS, this is the way to go. This does run on desktop platforms (Windows, macOS, Linux) as well but note that your application may not look and behave like a <em>native application</em>. However, there is a pretty large community around this framework and you can easily find resources to help you learn it online.</p>
<p>The look and feel of Kivy is extremely customizable, allowing it to be used as an alternative to libraries like Pygame (for making games with Python). The developers have also released a number of separate libraries for Kivy. Some provide Kivy with better integration and access to certain platform-specific features, or help package your application for distribution on platforms like Android and iOS. Kivy has it's own design language called Kv, which is similar to QML for Qt. It allows you to easily separate the interface design from your application's logic.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> There is a 3rd party add-on for Kivy named KivyMD that replaces Kivy's widgets with ones that are compliant with Google's Material Design.</p>
<p>A simple <em>hello world</em> application in Kivy is shown below.</p>
<p><strong>Installation</strong> <code>pip install kivy</code></p>
<p>A simple <em>hello world</em> application in Kivy is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.core.window import Window
Window.size = (300, 200)
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
self.button = Button(text="Hello, World?")
self.button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
def handle_button_clicked(self, event):
self.button.text = "Hello, World!"
class MyApp(App):
def build(self):
self.title = "Hello, World!"
return MainWindow()
app = MyApp()
app.run()
</code></pre>
</div>
<p><img alt="Kivy Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/kivy-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-600 600w" loading="lazy" width="608" height="466"/>
<em>Hello world application built using Kivy, running on Windows 11</em></p>
<p>An equivalent application built using the Kv declarative language is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="087ac45ebe754674946e675a1012dcd7" v-on:click="switch_tab">main.py</li>
<li class="tab-link" data-tab="d2ad520bc73e498fb47dcbf6bb610f2b" v-on:click="switch_tab">controller.kv</li></ul><div class="tab-content current code-block-outer" id="087ac45ebe754674946e675a1012dcd7">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import kivy
kivy.require('1.0.5')
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty
class Controller(FloatLayout):
'''Create a controller that receives a custom widget from the kv lang file.
Add an action to be called from the kv lang file.
'''
def button_pressed(self):
self.button_wid.text = 'Hello, World!'
class ControllerApp(App):
def build(self):
return Controller()
if __name__ == '__main__':
ControllerApp().run()
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="d2ad520bc73e498fb47dcbf6bb610f2b">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">#:kivy 1.0
<controller>:
button_wid: custom_button
BoxLayout:
orientation: 'vertical'
padding: 20
Button:
id: custom_button
text: 'Hello, World?'
on_press: root.button_pressed()
</controller></code></pre>
</div>
</div>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The name of the Kv file <em>must</em> match the name of the class from the main application -- here <code>Controller</code> and <code>controller.kv</code>.</p>
<p><img alt="Kivy Kv Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/kivykv-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-600 600w" loading="lazy" width="1131" height="912"/>
<em>Hello world application built using Kivy + Kv, running on Windows 11</em></p>
<p>In February 2011, Kivy was released as the spiritual successor to a similar framework called <strong>PyMT</strong>. While they shared similar goals and was also led by the current core developers of Kivy, where Kivy differs is in its underlying design and a professional organization which actively develops and maintains it. Kivy is licensed under the MIT license, which is a 'permissive' license that allows you to use it freely in both open source and proprietary applications. As such, you are even allowed to make proprietary modifications to the framework itself.</p>
<ul>
<li><a href="https://kivy.org">Kivy Website</a></li>
<li><a href="https://kivy.org/doc/stable/">Kivy Documentation</a></li>
<li><a href="https://github.com/kivy">GitHub Repository</a></li>
<li><a href="https://kivymd.readthedocs.io/en/latest/">KivyMD Documentation</a></li>
<li><a href="https://kivy.org/doc/stable/guide/lang.html">Kv language documentation</a></li>
</ul>
<h2 id="pysimplegui">PySimpleGUI</h2>
<p><em><strong>Best for</strong> quickly building UIs for simple tools, very portable</em></p>
<p class="admonition admonition-warning"><span class="admonition-kind"><i class="fas fa-exclamation-circle"></i></span> PySimpleGUI 5 uses a paid subscription model for commercial software. Non-commercial distribution requires both developers and recipients to have an active PySimpleGUI subscription.</p>
<p>PySimpleGUI aims to simplify GUI application development for Python. It doesn't reinvent the wheel but provides a wrapper around other existing frameworks such as Tkinter, Qt (PySide 2), WxPython and Remi. By doing so, it lowers the barrier to creating a GUI but also allows you to easily migrate from one GUI framework to another by simply changing the import statement.</p>
<p>While there is a separate port of PySimpleGUI for each of these frameworks, the Tkinter version is considered the most feature complete. Wrapping other libraries comes at a cost however — your applications will not be able to exploit the full capabilities or performance of the underlying libraries. The pure-Python event loop can also hinder performance by bottlenecking events with the GIL. However, this is only really a concern when working with live data visualization, streaming or multimedia applications.</p>
<p>There is a fair amount of good resources to help you learn to use PySimpleGUI, including an official Cookbook and a Udemy course offered by the developers themselves. According to their project website, PySimpleGUI was initially made (and later released in 2018) because the lead developer wanted a 'simplified' GUI framework to use in his upcoming project and wasn't able to find any that met his needs.</p>
<p><strong>Installation</strong> <code>pip install pysimplegui</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import PySimpleGUI as sg
layout = [
[sg.Button("My simple app.")]
]
window = sg.Window("Hello World", layout)
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
</code></pre>
</div>
<p><img alt="PySimpleGUI Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pysimplegui-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-600 600w" loading="lazy" width="401" height="261"/>
<em>Hello world application built using PySimpleGUI, running on Windows 11</em></p>
<p>PySimpleGUI is licensed under the same LGPL v3 license as PySide, which allows its use in proprietary applications but modifications to the framework itself must be released as open source.</p>
<ul>
<li><a href="https://pysimplegui.readthedocs.io/en/latest/">PySimpleGUI Website</a></li>
<li><a href="https://www.pysimplegui.org/en/latest/cookbook/">PySimpleGUI Cookbook</a></li>
<li><a href="https://github.com/PySimpleGUI/PySimpleGUI">GitHub Repository</a></li>
</ul>
<h2 id="wxpython">WxPython</h2>
<p><em><strong>Best for</strong> simple portable desktop applications</em></p>
<p>WxPython is a wrapper for the popular, cross-platform GUI toolkit called WxWidgets. It is implemented as a set of Python extension modules that wrap the GUI components of the popular wxWidgets cross platform library, which is written in C++.</p>
<p>WxPython uses native widgets on most platforms, ensure that your application looks and feels at home.
However, WxPython is known to have certain platform-specific quirks and it also doesn't provide the same level of abstraction between platforms as Qt for example. This may affect how easy it is to maintain cross-platform compatibility for your application.</p>
<p>WxPython is under active development and is also currently being <em>reimplemented from scratch</em> under the name 'WxPython Phoenix'. The team behind WxWidgets is also responsible for WxPython, which was initially released in 1998.</p>
<p><strong>Installation</strong> <code>pip install wxpython</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import wx
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.button = wx.Button(self, label="My simple app.")
self.Bind(
wx.EVT_BUTTON, self.handle_button_click, self.button
)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
def handle_button_click(self, event):
self.Close()
app = wx.App(False)
w = MainWindow(None, "Hello World")
app.MainLoop()
</code></pre>
</div>
<p><img alt="WxPython Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/wxpython-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-600 600w" loading="lazy" width="407" height="491"/>
<em>Hello world application built using WxPython, running on Windows 11</em></p>
<p>Both WxWidgets and WxPython are licensed under a WxWindows Library License, which is a 'free software' license similar to LGPL (with a special exception). It allows both proprietary and open source applications to <em>use and modify</em> WxPython.</p>
<ul>
<li><a href="https://www.wxpython.org/">WxPython Website</a></li>
<li><a href="https://wiki.wxpython.org/">WxPython Wiki</a></li>
<li><a href="https://github.com/wxWidgets/Phoenix/">GitHub Repository</a></li>
</ul>
<h2 id="pygobject-gtk">PyGObject (GTK+)</h2>
<p><em><strong>Best for</strong> developing applications for GNOME desktop</em></p>
<p>If you intend to create an application that integrates well with <em>GNOME</em> and other GTK-based desktop environments for Linux, PyGObject is the right choice. PyGObject itself is a language-binding to the GTK+ widget toolkit. It allows you to create modern, adaptive user interfaces that conform to <a href="https://developer.gnome.org/hig/">GNOME's Human Interface Guidelines (HIG)</a>.</p>
<p>It also enables the development of 'convergent' applications that can run on both Linux desktop and mobile platforms. There are a few first-party and community-made, third-party tools available for it as well. This includes the likes of GNOME Builder and Glade, which is yet another WYSIWYG editor for building graphical interfaces quickly and easily.</p>
<p>Unfortunately, there aren't a whole lot of online resources to help you learn PyGObject application development, apart from <a href="https://python-gtk-3-tutorial.readthedocs.io/">this one rather well-documented tutorial</a>. While cross-platform support does exist (e.g. Inkscape, GIMP), the resulting applications won't feel completely native on other desktops. Setting up a development environment for this, especially on Windows and macOS, also requires more steps than for most other frameworks in this article, which just need a working Python installation.</p>
<p><strong>Installation</strong> Ubuntu/Debian <code>sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-4.0</code>, macOS Homebrew <code>brew install pygobject4 gtk+4</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
def on_activate(app):
win = Gtk.ApplicationWindow(application=app)
btn = Gtk.Button(label="Hello, World!")
btn.connect('clicked', lambda x: win.close())
win.set_child(btn)
win.present()
app = Gtk.Application(application_id='org.gtk.Example')
app.connect('activate', on_activate)
app.run(None)
</code></pre>
</div>
<p><img alt="PyGObject Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/gobject-ubuntu.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-600 600w" loading="lazy" width="367" height="178"/>
<em>Hello world application built using PyGObject, running on Ubuntu Linux 21.10</em></p>
<p>PyGObject is developed and maintained under the GNOME Foundation, who is also responsible for the GNOME desktop environment. PyGObject replaces several separate Python modules, including PyGTK, GIO and python-gnome, which were previously required to create a full GNOME/GTK application. Its initial release was in 2006 and it is licensed under an older version of LGPL (v2.1). While there are some differences with the current version of LGPL (v3), the license still allows its use in proprietary applications but requires any modification to the library itself to be released as open source.</p>
<ul>
<li><a href="https://www.gtk.org/docs/language-bindings/python/">PyGObject Project Homepage</a></li>
<li><a href="https://pygobject.readthedocs.io/">PyGObject Documentation</a></li>
<li><a href="https://gitlab.gnome.org/GNOME/pygobject/">GitLab Repository</a></li>
</ul>
<h2 id="remi">Remi</h2>
<p><em><strong>Best for</strong> web based UIs for Python applications</em></p>
<p>Remi, which stands for <em>REMote Interface</em>, is the ideal solution for applications that are intended to be run on servers and other headless setups. (For example, on a Raspberry Pi.) Unlike most other GUI frameworks/libraries, Remi is rendered completely in the browser using a built-in web server. Hence, it is completely platform-independent and runs equally well on all platforms.</p>
<p>That also makes the application's interface accessible to any computer or device with a web browser that is connected to the same network. Although access can be restricted with a username and password, it doesn't implement any security strategies by default. Note that Remi is meant to be used as a <em>desktop GUI framework</em> and not for <em>serving up web pages</em>. If more than one user connects to the application at the same time, they will see and interact with the exact same things as if a single user was using it.</p>
<p>Remi requires no prior knowledge of HTML or other similar web technologies. You only need to have a working understanding of Python to use it, which is then automatically translated to HTML. It also comes included with a <em>drag n drop GUI editor</em> that is akin to Qt Designer for PyQt and PySide.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import remi.gui as gui
from remi import start, App
class MyApp(App):
def main(self):
container = gui.VBox(width=120, height=100)
# Create a button, with the label "Hello, World!"
self.bt = gui.Button('Hello, World?')
self.bt.onclick.do(self.on_button_pressed)
# Add the button to the container, and return it.
container.append(self.bt)
return container
def on_button_pressed(self, widget):
self.bt.set_text('Hello, World!')
start(MyApp)
</code></pre>
</div>
<p>Remi is licensed under the Apache License v2.0, which is another 'permissive' license similar to the MIT License. The license allows using it in both open source and proprietary applications, while also allowing proprietary modifications to be made to the framework itself. Its main conditions revolve around the preservation of copyright and license notices.</p>
<p><img alt="Remi Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/remi-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-600 600w" loading="lazy" width="347" height="428"/>
<em>Hello world application built using Remi, running on Chrome on Windows 11</em></p>
<ul>
<li><a href="https://github.com/rawpython/remi">GitHub Repository</a></li>
<li><a href="https://www.reddit.com/r/RemiGUI">Reddit (r/RemiGUI)</a></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>If you're looking to build GUI applications with Python, there is probably a GUI framework/library listed here that fits the bill for your project. Try and weigh up the capabilities & licensing of the different libraries with the scale of your project, both now and in the future.</p>
<p>Don't be afraid to experiment a bit with different libraries, to see which feel the best fit.
While the APIs of GUI libraries are very different, they share many underlying concepts in common
and things you learn in one library will often apply in another.</p>
<p>You are only limited by your own imagination. So go out there and make something!</p><p>Python is a popular programming used for everything from scripting routine tasks to building websites and performing complex data analysis. While you can accomplish a lot with command line tools, some tasks are better suited to graphical interfaces. You may also find yourself wanting to build a desktop front-end for an existing tool to improve usability for non-technical users. Or maybe you're building some hardware or a mobile app and want an intuitive touchscreen interface.</p>
<p>To create <em>graphical user interfaces</em> with Python, you need a GUI library. Unfortunately, at this point things
get pretty confusing -- there are many different GUI libraries available for Python, all with different capabilities and licensing. <em>Which Python GUI library should you use for your project?</em></p>
<p>In this article, we will look at a selection of the most popular Python GUI frameworks currently available and why you should consider using them for your own projects. You'll learn about the relative strengths of each library, understand the licensing limitations and see a simple <em>Hello, World!</em> application written in each. By the end of the article you should feel confident choosing the right library for your project.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> <strong>tldr</strong> If you're looking to build professional quality software, start with PySide6 or PyQt6. The Qt framework is batteries-included — whatever your project, you'll be able to get it done. We have a complete <a href="https://www.pythonguis.com/pyside6-tutorial">PySide6 tutorial</a> and <a href="https://www.pythonguis.com/pyqt6-tutorial">PyQt6 tutorial</a> as well as a Github respository full of <a href="https://github.com/pythonguis/pythonguis-examples">Python GUI examples</a> to get you started.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#tkinter">Tkinter</a></li>
<li><a href="#pyqt-or-pyside">PyQt or PySide</a></li>
<li><a href="#pyqtpyside-with-qml">PyQt/PySide with QML</a></li>
<li><a href="#kivy">Kivy</a></li>
<li><a href="#pysimplegui">PySimpleGUI</a></li>
<li><a href="#wxpython">WxPython</a></li>
<li><a href="#pygobject-gtk">PyGObject (GTK+)</a></li>
<li><a href="#remi">Remi</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="tkinter">Tkinter</h2>
<p><em><strong>Best for</strong> simple tool GUIs, small portable applications</em></p>
<p>Tkinter is the defacto GUI framework for Python. It comes bundled with Python on both Windows and macOS. (On Linux, it may require downloading an additional package from your distribution's repo.) Tkinter is a wrapper written around the Tk GUI toolkit. Its name is an amalgamation of the words <em>Tk</em> and <em>Interface</em>.</p>
<p>Tkinter is a simple library with support for standard layouts and widgets, as well as more complex widgets such as tabbed views & progressbars. Tkinter is a pure GUI library, not a framework. There is no built-in support for GUIs driven from data sources, databases, or for displaying or manipulating multimedia or hardware. However, if you need to make something simple that doesn't require any additional dependencies, Tkinter may be what you are looking for. Tkinter is cross-platform however the widgets can look outdated, particularly on Windows.</p>
<p><strong>Installation</strong> Already installed with Python on Windows and macOS. Ubuntu/Debian Linux <code>sudo apt install python3-tk</code></p>
<p>A simple <em>hello world</em> application in Tkinter is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="c8714a57212741f2b2e5a4d84e703cdf" v-on:click="switch_tab">Standard</li>
<li class="tab-link" data-tab="c2015f5d4305481d88b8aa94ef2619a8" v-on:click="switch_tab">Class-based</li></ul><div class="tab-content current code-block-outer" id="c8714a57212741f2b2e5a4d84e703cdf">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import tkinter as tk
window = tk.Tk()
window.title("Hello World")
def handle_button_press(event):
window.destroy()
button = tk.Button(text="My simple app.")
button.bind("<button-1>", handle_button_press)
button.pack()
# Start the event loop.
window.mainloop()
</button-1></code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="c2015f5d4305481d88b8aa94ef2619a8">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from tkinter import Tk, Button
class Window(Tk):
def __init__(self):
super().__init__()
self.title("Hello World")
self.button = Button(text="My simple app.")
self.button.bind("<button-1>", self.handle_button_press)
self.button.pack()
def handle_button_press(self, event):
self.destroy()
# Start the event loop.
window = Window()
window.mainloop()
</button-1></code></pre>
</div>
</div>
</div>
<p><img alt="Tkinter Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/tkinter-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/tkinter-windows.jpg?tr=w-600 600w" loading="lazy" width="387" height="218"/>
<em>Hello world application built using Tkinter, running on Windows 11</em></p>
<p>Tkinter was originally developed by Steen Lumholt and Guido Van Rossum, who designed Python itself. Both the GUI framework and the language are licensed under the same Python Software Foundation (PSF) License. While the license is compatible with the GPL, it is a 'permissive' license (similar to the MIT License) that allows it to be used for proprietary applications and modifications.</p>
<ul>
<li><a href="https://www.pythonguis.com/tkinter/">Tkinter tutorial</a></li>
<li><a href="https://docs.python.org/3/library/tkinter.html">Tkinter Documentation</a></li>
</ul>
<h2 id="pyqt-or-pyside">PyQt or PySide</h2>
<p><em><strong>Best for</strong> commercial, multimedia, scientific or engineering desktop applications</em></p>
<p>PyQt and PySide are wrappers around the Qt framework. They allow you to easily create modern interfaces that look right at home on any platform, including Windows, macOS, Linux and even Android. They also have solid tooling with the most notable being <em>Qt Creator</em>, which includes a WYSIWYG editor for designing GUI interfaces quickly and easily. Being backed by a commercial project means that you will find plenty of support and online learning resources to help you develop your application.</p>
<p>Qt (and by extension PyQt & PySide) is not just a GUI library, but a complete application development framework. In addition to standard UI elements, such as widgets and layouts, Qt provides MVC-like data-driven views (spreadsheets, tables), database interfaces & models, graph plotting, vector graphics visualization, multimedia playback, sound effects & playlists and built-in interfaces for hardware such as printing. The Qt signals and slots models allows large applications to be built from re-usable and isolated components.</p>
<p>While other toolkits can work great when building small & simple tools, Qt really comes into its own for building real <em>commercial-quality</em> applications where you will benefit from the pre-built components. This comes at the expense of a slight learning curve. However, for smaller projects Qt is not really any more complex than other libraries.
Qt Widgets-based applications use platform native widgets to ensure they look and feel at home on Windows, macOS and Qt-based Linux desktops.</p>
<p><strong>Installation</strong> <code>pip install pyqt6</code> or <code>pip install pyside6</code></p>
<p>A simple <em>hello world</em> application in PyQt6, using the Qt Widgets API is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="69d959c84c924e57937cbcb10ee49436" v-on:click="switch_tab">PyQt6</li>
<li class="tab-link" data-tab="a944bf84e8ad4c8fbba07493b46c0377" v-on:click="switch_tab">PySide6</li></ul><div class="tab-content current code-block-outer" id="69d959c84c924e57937cbcb10ee49436">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QMainWindow, QApplication, QPushButton
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
button = QPushButton("My simple app.")
button.pressed.connect(self.close)
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a944bf84e8ad4c8fbba07493b46c0377">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PySide6.QtWidgets import QMainWindow, QApplication, QPushButton
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
button = QPushButton("My simple app.")
button.pressed.connect(self.close)
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
</code></pre>
</div>
</div>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> As you can see, the code is almost identical between PyQt & PySide, so it's not something to be concerned about when you start developing with either: you can always migrate easily if you need to.</p>
<p><img alt="PyQt6 Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pyqt6-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6-windows.jpg?tr=w-600 600w" loading="lazy" width="473" height="346"/>
<em>Hello world application built using PyQt6, running on Windows 11</em></p>
<p>Before the Qt Company (under Nokia) released the officially supported PySide library in 2009, Riverbank Computing had released PyQt in 1998. The main difference between these two libraries is in <em>licensing</em>. The free-to-use version of PyQt is licensed under GNU General Public License (GPL) v3 but PySide is licensed under GNU Lesser General Public License (LGPL). This means that PyQt is limited GPL-licensed applications unless you purchase its commercial version, while PySide may be used in non-GPL applications without any additional fee. However, note that both these libraries are separate from Qt itself which also has a free-to-use, open source version and a paid, commercial version.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> For a more information see our article on <a href="https://www.pythonguis.com/faq/pyqt-vs-pyside/">PyQt vs PySide licensing</a>.</p>
<ul>
<li>
<p><strong>PySide6</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyside6-book/">PySide6 Book</a></li>
<li><a href="https://www.pythonguis.com/pyside6-tutorial/">PySide6 Tutorial</a></li>
<li><a href="https://www.qt.io/qt-for-python">PySide Website</a></li>
<li><a href="https://doc.qt.io/qtforpython/">PySide Documentation</a></li>
<li><a href="https://github.com/PySide">GitHub Repository</a></li>
</ul>
</li>
<li>
<p><strong>PyQt6</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyqt6-book/">PyQt6 Book</a></li>
<li><a href="https://www.pythonguis.com/pyqt6-tutorial/">PyQt6 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/software/pyqt/intro">PyQt Website</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/">PyQt6 Documentation</a></li>
</ul>
</li>
<li>
<p><strong>PyQt5</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/pyqt5-book/">PyQt5 Book</a></li>
<li><a href="https://www.pythonguis.com/pyqt5-tutorial/">PyQt5 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt5/">PyQt6 Documentation</a></li>
</ul>
</li>
</ul>
<h2 id="pyqtpyside-with-qml">PyQt/PySide with QML</h2>
<p><em><strong>Best for</strong> Raspberry Pi, microcontrollers, industrial and consumer electronics</em></p>
<p>When using PyQt and PySide you actually have <em>two</em> options for building your GUIs. We've already introduced the Qt Widgets API
which is well-suited for building desktop applications. But Qt also provides a <em>declarative</em> API in the form of Qt Quick/QML.</p>
<p>Using Qt Quick/QML you have access to the entire Qt framework for building your applications. Your UI consists of two parts: the Python code which
handles the business logic and the QML which defines the structure and behavior of the UI itself. You can control the UI from Python, or use
embedded Javascript code to handle events and animations.</p>
<p>Qt Quick/QML is ideally suited for building modern touchscreen interfaces for microcontrollers or device interfaces -- for example, building
interfaces for microcontrollers like the Raspberry Pi. However you can also use it on desktop to build completely customized application
experiences, like those you find in media player applications like Spotify, or to desktop games.</p>
<p><strong>Installation</strong> <code>pip install pyqt6</code> or <code>pip install pyside6</code></p>
<p>A simple <em>Hello World</em> app in PyQt6 with QML. Save the QML file in the same folder as the Python file, and run as normally.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="7bf228dde9544f2d9be1103bcefcf93b" v-on:click="switch_tab">main.py</li>
<li class="tab-link" data-tab="d2f226d74300490797add2fff8e9032b" v-on:click="switch_tab">main.qml</li></ul><div class="tab-content current code-block-outer" id="7bf228dde9544f2d9be1103bcefcf93b">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('main.qml')
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="d2f226d74300490797add2fff8e9032b">
<div class="code-block">
<span class="code-block-language code-block-qml">qml</span>
<pre><code class="qml">import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 600
height: 500
title: "HelloApp"
Text {
anchors.centerIn: parent
text: "Hello World"
font.pixelSize: 24
}
}
</code></pre>
</div>
</div>
</div>
<p>Licensing for Qt Quick/QML applications is the same as for other PyQt/PySide apps.</p>
<ul>
<li>
<p><strong>PyQt</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/tutorials/qml-qtquick-python-application/">QML/PyQt5 Tutorial</a></li>
<li><a href="https://www.pythonguis.com/tutorials/pyqt6-qml-qtquick-python-application/">QML/PyQt6 Tutorial</a></li>
<li><a href="https://www.riverbankcomputing.com/software/pyqt/intro">PyQt Website</a></li>
<li><a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/">PyQt6 Documentation</a></li>
</ul>
</li>
<li>
<p><strong>PySide</strong></p>
<ul>
<li><a href="https://www.pythonguis.com/tutorials/pyside-qml-qtquick-python-application/">QML/PySide2 Tutorial</a></li>
<li><a href="https://www.pythonguis.com/tutorials/pyside6-qml-qtquick-python-application/">QML/PySide6 Tutorial</a></li>
<li><a href="https://www.qt.io/qt-for-python">PySide Website</a></li>
<li><a href="https://doc.qt.io/qtforpython/">PySide Documentation</a></li>
<li><a href="https://github.com/PySide">GitHub Repository</a></li>
</ul>
</li>
</ul>
<p><img alt="PyQt6 QML Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pyqt6qml-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pyqt6qml-windows.jpg?tr=w-600 600w" loading="lazy" width="952" height="861"/>
<em>Hello world application built using PyQt6 & QML, running on Windows 11</em></p>
<h2 id="kivy">Kivy</h2>
<p><em><strong>Best for</strong> Python mobile app development</em></p>
<p>While most other GUI frameworks are <em>bindings</em> to toolkits written in other programming languages, Kivy is perhaps the only framework which is primarily written in pure Python. If you want to create touchscreen-oriented interfaces with a focus on mobile platforms such as Android and iOS, this is the way to go. This does run on desktop platforms (Windows, macOS, Linux) as well but note that your application may not look and behave like a <em>native application</em>. However, there is a pretty large community around this framework and you can easily find resources to help you learn it online.</p>
<p>The look and feel of Kivy is extremely customizable, allowing it to be used as an alternative to libraries like Pygame (for making games with Python). The developers have also released a number of separate libraries for Kivy. Some provide Kivy with better integration and access to certain platform-specific features, or help package your application for distribution on platforms like Android and iOS. Kivy has it's own design language called Kv, which is similar to QML for Qt. It allows you to easily separate the interface design from your application's logic.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> There is a 3rd party add-on for Kivy named KivyMD that replaces Kivy's widgets with ones that are compliant with Google's Material Design.</p>
<p>A simple <em>hello world</em> application in Kivy is shown below.</p>
<p><strong>Installation</strong> <code>pip install kivy</code></p>
<p>A simple <em>hello world</em> application in Kivy is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.core.window import Window
Window.size = (300, 200)
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
self.button = Button(text="Hello, World?")
self.button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
def handle_button_clicked(self, event):
self.button.text = "Hello, World!"
class MyApp(App):
def build(self):
self.title = "Hello, World!"
return MainWindow()
app = MyApp()
app.run()
</code></pre>
</div>
<p><img alt="Kivy Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/kivy-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivy-windows.jpg?tr=w-600 600w" loading="lazy" width="608" height="466"/>
<em>Hello world application built using Kivy, running on Windows 11</em></p>
<p>An equivalent application built using the Kv declarative language is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="087ac45ebe754674946e675a1012dcd7" v-on:click="switch_tab">main.py</li>
<li class="tab-link" data-tab="d2ad520bc73e498fb47dcbf6bb610f2b" v-on:click="switch_tab">controller.kv</li></ul><div class="tab-content current code-block-outer" id="087ac45ebe754674946e675a1012dcd7">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import kivy
kivy.require('1.0.5')
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty
class Controller(FloatLayout):
'''Create a controller that receives a custom widget from the kv lang file.
Add an action to be called from the kv lang file.
'''
def button_pressed(self):
self.button_wid.text = 'Hello, World!'
class ControllerApp(App):
def build(self):
return Controller()
if __name__ == '__main__':
ControllerApp().run()
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="d2ad520bc73e498fb47dcbf6bb610f2b">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">#:kivy 1.0
<controller>:
button_wid: custom_button
BoxLayout:
orientation: 'vertical'
padding: 20
Button:
id: custom_button
text: 'Hello, World?'
on_press: root.button_pressed()
</controller></code></pre>
</div>
</div>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The name of the Kv file <em>must</em> match the name of the class from the main application -- here <code>Controller</code> and <code>controller.kv</code>.</p>
<p><img alt="Kivy Kv Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/kivykv-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/kivykv-windows.jpg?tr=w-600 600w" loading="lazy" width="1131" height="912"/>
<em>Hello world application built using Kivy + Kv, running on Windows 11</em></p>
<p>In February 2011, Kivy was released as the spiritual successor to a similar framework called <strong>PyMT</strong>. While they shared similar goals and was also led by the current core developers of Kivy, where Kivy differs is in its underlying design and a professional organization which actively develops and maintains it. Kivy is licensed under the MIT license, which is a 'permissive' license that allows you to use it freely in both open source and proprietary applications. As such, you are even allowed to make proprietary modifications to the framework itself.</p>
<ul>
<li><a href="https://kivy.org">Kivy Website</a></li>
<li><a href="https://kivy.org/doc/stable/">Kivy Documentation</a></li>
<li><a href="https://github.com/kivy">GitHub Repository</a></li>
<li><a href="https://kivymd.readthedocs.io/en/latest/">KivyMD Documentation</a></li>
<li><a href="https://kivy.org/doc/stable/guide/lang.html">Kv language documentation</a></li>
</ul>
<h2 id="pysimplegui">PySimpleGUI</h2>
<p><em><strong>Best for</strong> quickly building UIs for simple tools, very portable</em></p>
<p class="admonition admonition-warning"><span class="admonition-kind"><i class="fas fa-exclamation-circle"></i></span> PySimpleGUI 5 uses a paid subscription model for commercial software. Non-commercial distribution requires both developers and recipients to have an active PySimpleGUI subscription.</p>
<p>PySimpleGUI aims to simplify GUI application development for Python. It doesn't reinvent the wheel but provides a wrapper around other existing frameworks such as Tkinter, Qt (PySide 2), WxPython and Remi. By doing so, it lowers the barrier to creating a GUI but also allows you to easily migrate from one GUI framework to another by simply changing the import statement.</p>
<p>While there is a separate port of PySimpleGUI for each of these frameworks, the Tkinter version is considered the most feature complete. Wrapping other libraries comes at a cost however — your applications will not be able to exploit the full capabilities or performance of the underlying libraries. The pure-Python event loop can also hinder performance by bottlenecking events with the GIL. However, this is only really a concern when working with live data visualization, streaming or multimedia applications.</p>
<p>There is a fair amount of good resources to help you learn to use PySimpleGUI, including an official Cookbook and a Udemy course offered by the developers themselves. According to their project website, PySimpleGUI was initially made (and later released in 2018) because the lead developer wanted a 'simplified' GUI framework to use in his upcoming project and wasn't able to find any that met his needs.</p>
<p><strong>Installation</strong> <code>pip install pysimplegui</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import PySimpleGUI as sg
layout = [
[sg.Button("My simple app.")]
]
window = sg.Window("Hello World", layout)
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
</code></pre>
</div>
<p><img alt="PySimpleGUI Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/pysimplegui-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/pysimplegui-windows.jpg?tr=w-600 600w" loading="lazy" width="401" height="261"/>
<em>Hello world application built using PySimpleGUI, running on Windows 11</em></p>
<p>PySimpleGUI is licensed under the same LGPL v3 license as PySide, which allows its use in proprietary applications but modifications to the framework itself must be released as open source.</p>
<ul>
<li><a href="https://pysimplegui.readthedocs.io/en/latest/">PySimpleGUI Website</a></li>
<li><a href="https://www.pysimplegui.org/en/latest/cookbook/">PySimpleGUI Cookbook</a></li>
<li><a href="https://github.com/PySimpleGUI/PySimpleGUI">GitHub Repository</a></li>
</ul>
<h2 id="wxpython">WxPython</h2>
<p><em><strong>Best for</strong> simple portable desktop applications</em></p>
<p>WxPython is a wrapper for the popular, cross-platform GUI toolkit called WxWidgets. It is implemented as a set of Python extension modules that wrap the GUI components of the popular wxWidgets cross platform library, which is written in C++.</p>
<p>WxPython uses native widgets on most platforms, ensure that your application looks and feels at home.
However, WxPython is known to have certain platform-specific quirks and it also doesn't provide the same level of abstraction between platforms as Qt for example. This may affect how easy it is to maintain cross-platform compatibility for your application.</p>
<p>WxPython is under active development and is also currently being <em>reimplemented from scratch</em> under the name 'WxPython Phoenix'. The team behind WxWidgets is also responsible for WxPython, which was initially released in 1998.</p>
<p><strong>Installation</strong> <code>pip install wxpython</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import wx
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.button = wx.Button(self, label="My simple app.")
self.Bind(
wx.EVT_BUTTON, self.handle_button_click, self.button
)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
def handle_button_click(self, event):
self.Close()
app = wx.App(False)
w = MainWindow(None, "Hello World")
app.MainLoop()
</code></pre>
</div>
<p><img alt="WxPython Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/wxpython-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/wxpython-windows.jpg?tr=w-600 600w" loading="lazy" width="407" height="491"/>
<em>Hello world application built using WxPython, running on Windows 11</em></p>
<p>Both WxWidgets and WxPython are licensed under a WxWindows Library License, which is a 'free software' license similar to LGPL (with a special exception). It allows both proprietary and open source applications to <em>use and modify</em> WxPython.</p>
<ul>
<li><a href="https://www.wxpython.org/">WxPython Website</a></li>
<li><a href="https://wiki.wxpython.org/">WxPython Wiki</a></li>
<li><a href="https://github.com/wxWidgets/Phoenix/">GitHub Repository</a></li>
</ul>
<h2 id="pygobject-gtk">PyGObject (GTK+)</h2>
<p><em><strong>Best for</strong> developing applications for GNOME desktop</em></p>
<p>If you intend to create an application that integrates well with <em>GNOME</em> and other GTK-based desktop environments for Linux, PyGObject is the right choice. PyGObject itself is a language-binding to the GTK+ widget toolkit. It allows you to create modern, adaptive user interfaces that conform to <a href="https://developer.gnome.org/hig/">GNOME's Human Interface Guidelines (HIG)</a>.</p>
<p>It also enables the development of 'convergent' applications that can run on both Linux desktop and mobile platforms. There are a few first-party and community-made, third-party tools available for it as well. This includes the likes of GNOME Builder and Glade, which is yet another WYSIWYG editor for building graphical interfaces quickly and easily.</p>
<p>Unfortunately, there aren't a whole lot of online resources to help you learn PyGObject application development, apart from <a href="https://python-gtk-3-tutorial.readthedocs.io/">this one rather well-documented tutorial</a>. While cross-platform support does exist (e.g. Inkscape, GIMP), the resulting applications won't feel completely native on other desktops. Setting up a development environment for this, especially on Windows and macOS, also requires more steps than for most other frameworks in this article, which just need a working Python installation.</p>
<p><strong>Installation</strong> Ubuntu/Debian <code>sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-4.0</code>, macOS Homebrew <code>brew install pygobject4 gtk+4</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
def on_activate(app):
win = Gtk.ApplicationWindow(application=app)
btn = Gtk.Button(label="Hello, World!")
btn.connect('clicked', lambda x: win.close())
win.set_child(btn)
win.present()
app = Gtk.Application(application_id='org.gtk.Example')
app.connect('activate', on_activate)
app.run(None)
</code></pre>
</div>
<p><img alt="PyGObject Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/gobject-ubuntu.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/gobject-ubuntu.jpg?tr=w-600 600w" loading="lazy" width="367" height="178"/>
<em>Hello world application built using PyGObject, running on Ubuntu Linux 21.10</em></p>
<p>PyGObject is developed and maintained under the GNOME Foundation, who is also responsible for the GNOME desktop environment. PyGObject replaces several separate Python modules, including PyGTK, GIO and python-gnome, which were previously required to create a full GNOME/GTK application. Its initial release was in 2006 and it is licensed under an older version of LGPL (v2.1). While there are some differences with the current version of LGPL (v3), the license still allows its use in proprietary applications but requires any modification to the library itself to be released as open source.</p>
<ul>
<li><a href="https://www.gtk.org/docs/language-bindings/python/">PyGObject Project Homepage</a></li>
<li><a href="https://pygobject.readthedocs.io/">PyGObject Documentation</a></li>
<li><a href="https://gitlab.gnome.org/GNOME/pygobject/">GitLab Repository</a></li>
</ul>
<h2 id="remi">Remi</h2>
<p><em><strong>Best for</strong> web based UIs for Python applications</em></p>
<p>Remi, which stands for <em>REMote Interface</em>, is the ideal solution for applications that are intended to be run on servers and other headless setups. (For example, on a Raspberry Pi.) Unlike most other GUI frameworks/libraries, Remi is rendered completely in the browser using a built-in web server. Hence, it is completely platform-independent and runs equally well on all platforms.</p>
<p>That also makes the application's interface accessible to any computer or device with a web browser that is connected to the same network. Although access can be restricted with a username and password, it doesn't implement any security strategies by default. Note that Remi is meant to be used as a <em>desktop GUI framework</em> and not for <em>serving up web pages</em>. If more than one user connects to the application at the same time, they will see and interact with the exact same things as if a single user was using it.</p>
<p>Remi requires no prior knowledge of HTML or other similar web technologies. You only need to have a working understanding of Python to use it, which is then automatically translated to HTML. It also comes included with a <em>drag n drop GUI editor</em> that is akin to Qt Designer for PyQt and PySide.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import remi.gui as gui
from remi import start, App
class MyApp(App):
def main(self):
container = gui.VBox(width=120, height=100)
# Create a button, with the label "Hello, World!"
self.bt = gui.Button('Hello, World?')
self.bt.onclick.do(self.on_button_pressed)
# Add the button to the container, and return it.
container.append(self.bt)
return container
def on_button_pressed(self, widget):
self.bt.set_text('Hello, World!')
start(MyApp)
</code></pre>
</div>
<p>Remi is licensed under the Apache License v2.0, which is another 'permissive' license similar to the MIT License. The license allows using it in both open source and proprietary applications, while also allowing proprietary modifications to be made to the framework itself. Its main conditions revolve around the preservation of copyright and license notices.</p>
<p><img alt="Remi Application Screenshot" src="https://www.pythonguis.com/static/faq/python-gui-libraries/remi-windows.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/python-gui-libraries/remi-windows.jpg?tr=w-600 600w" loading="lazy" width="347" height="428"/>
<em>Hello world application built using Remi, running on Chrome on Windows 11</em></p>
<ul>
<li><a href="https://github.com/rawpython/remi">GitHub Repository</a></li>
<li><a href="https://www.reddit.com/r/RemiGUI">Reddit (r/RemiGUI)</a></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>If you're looking to build GUI applications with Python, there is probably a GUI framework/library listed here that fits the bill for your project. Try and weigh up the capabilities & licensing of the different libraries with the scale of your project, both now and in the future.</p>
<p>Don't be afraid to experiment a bit with different libraries, to see which feel the best fit.
While the APIs of GUI libraries are very different, they share many underlying concepts in common
and things you learn in one library will often apply in another.</p>
<p>You are only limited by your own imagination. So go out there and make something!</p>Plotting With PyQtGraph — Create Custom Plots in PyQt6 With PyQtGraph2024-02-19T06:00:00+00:002024-02-19T06:00:00+00:00John Limtag:www.pythonguis.com,2024-02-19:/tutorials/pyqt6-plotting-pyqtgraph/<p>One of the major fields where Python shines is in data science. For data exploration and cleaning, Python has many powerful tools, such as <a href="https://pandas.pydata.org/">pandas</a> and <a href="https://pypi.org/project/polar/">polar</a>. For visualization, Python has Matplotlib.</p>
<p>When you're building GUI applications with PyQt, you can have access to all those tools directly from within your app. While it is possible to embed <code>matplotlib</code> plots in PyQt, the experience doesn't feel entirely <em>native</em>. So, for highly integrated plots, you may want to consider using the <a href="https://www.pyqtgraph.org/">PyQtGraph</a> library instead.</p>
<p>PyQtGraph is built on top of Qt's native <code>QGraphicsScene</code>, so it gives better drawing performance, particularly for live data. It also provides interactivity and the ability to customize plots according to your needs.</p>
<p>In this tutorial, you'll learn the basics of creating plots with PyQtGraph. You'll also explore the different plot customization options, including background color, line colors, line type, axis labels, and more.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#installing-pyqtgraph">Installing PyQtGraph</a></li>
<li><a href="#creating-a-plotwidget-instance">Creating a PlotWidget Instance</a></li>
<li><a href="#customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</a><ul>
<li><a href="#background-color">Background Color</a></li>
<li><a href="#line-color-width-and-style">Line Color, Width, and Style</a></li>
<li><a href="#line-markers">Line Markers</a></li>
<li><a href="#plot-titles">Plot Titles</a></li>
<li><a href="#axis-labels">Axis Labels</a></li>
<li><a href="#plot-legends">Plot Legends</a></li>
<li><a href="#background-grid">Background Grid</a></li>
<li><a href="#axis-range">Axis Range</a></li>
</ul>
</li>
<li><a href="#multiple-plot-lines">Multiple Plot Lines</a></li>
<li><a href="#creating-dynamic-plots">Creating Dynamic Plots</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="installing-pyqtgraph">Installing PyQtGraph</h2>
<p>To use PyQtGraph with PyQt6, you first need to install the library in your Python environment. You can do this using <code>pip</code> as follows:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m pip install pyqtgraph
</code></pre>
</div>
<p>Once the installation is complete, you will be able to import the module into your Python code. So, now you are ready to start creating plots.</p>
<h2 id="creating-a-plotwidget-instance">Creating a <code>PlotWidget</code> Instance</h2>
<p>In PyQtGraph, all plots use the <a href="https://pyqtgraph.readthedocs.io/en/latest/api_reference/widgets/plotwidget.html"><code>PlotWidget</code></a> class. This widget provides a <em>canvas</em> on which we can add and configure many types of plots. Under the hood, <code>PlotWidget</code> uses Qt's <code>QGraphicsScene</code> class, meaning that it's fast, efficient, and well-integrated with the rest of your app.</p>
<p>The code below shows a basic GUI app with a single <code>PlotWidget</code> in a <code>QMainWindow</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt6 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(minutes, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>In this short example, you create a PyQt app with a <code>PlotWidget</code> as its central widget. Then you create two lists of sample data for time and temperature. The final step to create the plot is to call the <code>plot()</code> methods with the data you want to visualize.</p>
<p>The first argument to <code>plot()</code> will be your <code>x</code> coordinate, while the second argument will be the <code>y</code> coordinate.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In all the examples in this tutorial, we import PyQtGraph using <code>import pyqtgraph as pg</code>. This is a common practice in PyQtGraph examples to keep things tidy and reduce typing.</p>
<p>If you run the above application, then you'll get the following window on your screen:</p>
<p><img alt="Basic PyQtGraph plot: Temperature vs time" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>Basic PyQtGraph plot: Temperature vs time.</em></p>
<p>PyQtGraph's default plot style is quite basic — a black background with a thin (barely visible) white line. Fortunately, the library provides several options that will allow us to deeply customize our plots.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In the examples in this tutorial, we'll create the PyQtGraph widget in code. To learn how to embed PyQtGraph plots when using Qt Designer, check out <a href="https://www.pythonguis.com/tutorials/embed-pyqtgraph-custom-widgets-qt-app/">Embedding custom widgets from Qt Designer</a>.</p>
<p>In the following section, we'll learn about the options we have available in PyQtGraph to improve the appearance and usability of our plots.</p>
<h2 id="customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</h2>
<p>Because PyQtGraph uses Qt's <code>QGraphicsScene</code> to render the graphs, we have access to all the standard Qt line and shape styling options for use in plots. PyQtGraph provides an <a href="https://en.wikipedia.org/wiki/API">API</a> for using these options to draw plots and manage the plot canvas.</p>
<p>Below, we'll explore the most common styling features that you'll need to create and customize your own plots with PyQtGraph.</p>
<h3 id="background-color">Background Color</h3>
<p>Beginning with the app skeleton above, we can change the background color by calling <code>setBackground()</code> on our <code>PlotWidget</code> instance, <code>self.graphWidget</code>. The code below sets the background to white by passing in the string <code>"w"</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(minutes, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Calling <code>setBackground()</code> with <code>"w"</code> as an argument changes the background of your plot to white, as you can see in the following window:</p>
<p><img alt="PyQtGraph plot with a white background" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a white background.</em></p>
<p>There are a number of colors available using single letters, as we did in the example above. They're based on the standard colors used in Matplotlib. Here are the most common codes:</p>
<table>
<thead>
<tr>
<th>Letter Code</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"b"</code></td>
<td>Blue</td>
</tr>
<tr>
<td><code>"c"</code></td>
<td>Cian</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Grey</td>
</tr>
<tr>
<td><code>"g"</code></td>
<td>Green</td>
</tr>
<tr>
<td><code>"k"</code></td>
<td>Black</td>
</tr>
<tr>
<td><code>"m"</code></td>
<td>Magenta</td>
</tr>
<tr>
<td><code>"r"</code></td>
<td>Red</td>
</tr>
<tr>
<td><code>"w"</code></td>
<td>White</td>
</tr>
<tr>
<td><code>"y"</code></td>
<td>Yellow</td>
</tr>
</tbody>
</table>
<p>In addition to these single-letter codes, we can create custom colors using the <a href="https://en.wikipedia.org/wiki/Web_colors#Hex_triplet">hexadecimal notation</a> as a string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground("#bbccaa") # Hex
</code></pre>
</div>
<p>We can also use <a href="https://en.wikipedia.org/wiki/RGB_color_model">RGB</a> and <a href="https://en.wikipedia.org/wiki/RGBA_color_model">RGBA</a> values passed in as 3-value and 4-value tuples, respectively. We must use values in the range from 0 to 255:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground((100, 50, 255)) # RGB each 0-255
self.plot_graph.setBackground((100, 50, 255, 25)) # RGBA (A = alpha opacity)
</code></pre>
</div>
<p>The first call to <code>setBackground()</code> takes a tuple representing an RGB color, while the second call takes a tuple representing an RGBA color.</p>
<p>We can also specify colors using Qt's <code>QColor</code> class if we prefer it:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtGui
# ...
self.plot_graph.setBackground(QtGui.QColor(100, 50, 254, 25))
</code></pre>
</div>
<p>Using <code>QColor</code> can be useful when you're using specific <code>QColor</code> objects elsewhere in your application and want to reuse them in your plots. For example, say that your app has a custom window background color, and you want to use it in the plots as well. Then you can do something like the following:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">color = self.palette().color(QtGui.QPalette.Window)
# ...
self.plot_graph.setBackground(color)
</code></pre>
</div>
<p>In the first line, you get the GUI's background color, while in the second line, you use that color for your plots.</p>
<h3 id="line-color-width-and-style">Line Color, Width, and Style</h3>
<p>Plot lines in PyQtGraph are drawn using the Qt <code>QPen</code> class. This gives us full control over line drawing, as we would have in any other <code>QGraphicsScene</code> drawing. To use a custom pen, you need to create a new <code>QPen</code> instance and pass it into the <code>plot()</code> method.</p>
<p>In the app below, we use a custom <code>QPen</code> object to change the line color to red:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature, pen=pen)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Here, we create a <code>QPen</code> object, passing in a 3-value tuple that defines an RGB red color. We could also define this color with the <code>"r"</code> code or with a <code>QColor</code> object. Then, we pass the pen to <code>plot()</code> with the <code>pen</code> argument.</p>
<p><img alt="PyQtGraph plot with a red plot line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red plot line.</em></p>
<p>By tweaking the <code>QPen</code> object, we can change the appearance of the line. For example, you can change the line width in pixels and the style (dashed, dotted, etc.), using Qt's line styles.</p>
<p>Update the following lines of code in your app to create a red, dashed line with 5 pixels of width:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtCore, QtWidgets
# ...
pen = pg.mkPen(color=(255, 0, 0), width=5, style=QtCore.Qt.DashLine)
</code></pre>
</div>
<p>The result of this code is shown below, giving a 5-pixel, dashed, red line:</p>
<p><img alt="PyQtGraph plot with a red, dashed, and 5-pixel line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red, dashed, and 5-pixel line</em></p>
<p>You can use all other Qt's line styles, including <code>Qt.SolidLine</code>, <code>Qt.DotLine</code>, <code>Qt.DashDotLine</code>, and <code>Qt.DashDotDotLine</code>. Examples of each of these lines are shown in the image below:</p>
<p><img alt="Qt Line Types" src="https://www.pythonguis.com/tutorials/pyqt6-plotting-pyqtgraph/Qt_Line_Types.png"/>
<em>Qt's line styles.</em></p>
<p>To learn more about Qt's line styles, check the <a href="https://doc.qt.io/qt-5/qpen.html#pen-style">documentation</a> about pen styles. There, you'll all you need to deeply customize the lines in your PyQtGraph plots.</p>
<h3 id="line-markers">Line Markers</h3>
<p>For many plots, it can be helpful to use point markers in addition or instead of lines on the plot. To draw a marker on your plot, pass the symbol you want to use as a marker when calling <code>plot()</code>. The following example uses the plus sign as a marker:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.plot(hour, temperature, symbol="+")
</code></pre>
</div>
<p>In this line of code, you pass a plus sign to the <code>symbol</code> argument. This tells PyQtGraph to use that symbol as a marker for the points in your plot.</p>
<p>If you use a custom <code>symbol</code>, then you can also use the <code>symbolSize</code>, <code>symbolBrush</code>, and <code>symbolPen</code> arguments to further customize the marker.</p>
<p>The value passed as <code>symbolBrush</code> can be any color, or <code>QBrush</code> instance, while <code>symbolPen</code> can be any color or a <code>QPen</code> instance. The pen is used to draw the shape, while the brush is used for the fill.</p>
<p>Go ahead and update your app's code to use a blue marker of size 15, on a red line:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.plot(
time,
temperature,
pen=pen,
symbol="+",
symbolSize=20,
symbolBrush="b",
)
</code></pre>
</div>
<p>In this code, you pass a plus sign to the <code>symbol</code> argument. You also customize the marker size and color. The resulting plot looks something like this:</p>
<p><img alt="PyQtGraph plot with a plus sign as a point marker" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a plus sign as a point marker.</em></p>
<p>In addition to the <code>+</code> plot marker, PyQtGraph supports the markers shown in the table below:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>Marker Shape</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"o"</code></td>
<td>Circle</td>
</tr>
<tr>
<td><code>"s"</code></td>
<td>Square</td>
</tr>
<tr>
<td><code>"t"</code></td>
<td>Triangle</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Diamond</td>
</tr>
<tr>
<td><code>"+"</code></td>
<td>Plus</td>
</tr>
<tr>
<td><code>"t1"</code></td>
<td>Triangle pointing upwards</td>
</tr>
<tr>
<td><code>"t2"</code></td>
<td>Triangle pointing right side</td>
</tr>
<tr>
<td><code>"t3"</code></td>
<td>Triangle pointing left side</td>
</tr>
<tr>
<td><code>"p"</code></td>
<td>Pentagon</td>
</tr>
<tr>
<td><code>"h"</code></td>
<td>Hexagon</td>
</tr>
<tr>
<td><code>"star"</code></td>
<td>Star</td>
</tr>
<tr>
<td><code>"x"</code></td>
<td>Cross</td>
</tr>
<tr>
<td><code>"arrow_up"</code></td>
<td>Arrow Up</td>
</tr>
<tr>
<td><code>"arrow_right"</code></td>
<td>Arrow Right</td>
</tr>
<tr>
<td><code>"arrow_down"</code></td>
<td>Arrow Down</td>
</tr>
<tr>
<td><code>"arrow_left"</code></td>
<td>Arrow Left</td>
</tr>
<tr>
<td><code>"crosshair"</code></td>
<td>Crosshair</td>
</tr>
</tbody>
</table>
<p>You can use any of these symbols as markers for your data points. If you have more specific marker requirements, then you can also use a <code>QPainterPath</code> object, which allows you to draw completely custom marker shapes.</p>
<h3 id="plot-titles">Plot Titles</h3>
<p>Plot titles are important to provide context around what is shown on a given chart. In PyQtGraph, you can add a main plot title using the <code>setTitle()</code> method on the <code>PlotWidget</code> object. Your title can be a regular Python string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time")
</code></pre>
</div>
<p>You can style your titles and change their font color and size by passing additional arguments to <code>setTitle()</code>. The code below sets the color to blue and the font size to 20 points:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
</code></pre>
</div>
<p>In this line of code, you set the title's font color to blue and the size to 20 points using the <code>color</code> and <code>size</code> arguments of <code>setTitle()</code>.</p>
<p>You could've even used CSS style and basic HTML tag syntax if you prefer, although it's less readable:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle(
'<span style="color: blue; font-size: 20pt">Temperature vs Time</span>'
)
</code></pre>
</div>
<p>In this case, you use a <code>span</code> HTML tag to wrap the title and apply some CSS styles on to of it. The final result is the same as suing the <code>color</code> and <code>size</code> arguments. Your plot will look like this:</p>
<p><img alt="PyQtGraph plot with title" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with title.</em></p>
<p>Your plot looks way better now. You can continue customizing it by adding informative lables to both axis.</p>
<h3 id="axis-labels">Axis Labels</h3>
<p>When it comes to axis labels, we can use the <code>setLabel()</code> method to create them. This method requires two arguments, <code>position</code> and <code>text</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel("left", "Temperature (°C)")
self.plot_graph.setLabel("bottom", "Time (min)")
</code></pre>
</div>
<p>The <code>position</code> argument can be any one of <code>"left"</code>, <code>"right"</code>, <code>"top"</code>, or <code>"bottom"</code>. They define the position of the axis on which the text is placed. The second argument, <code>text</code> is the text you want to use for the label.</p>
<p>You can pass an optional <code>style</code> argument into the <code>setLabel()</code> method. In this case, you need to use valid CSS name-value pairs. To provide these CSS pairs, you can use a dictionary:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
</code></pre>
</div>
<p>Here, you first create a dictionary containing CSS pairs. Then you pass this dictionary as an argument to the <code>setLabel()</code> method. Note that you need to use the dictionary unpacking operator to unpack the styles in the method call.</p>
<p>Again, you can use basic HTML syntax and CSS for the labels if you prefer:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel(
"left",
'<span style="color: red; font-size: 18px">Temperature (°C)</span>'
)
self.plot_graph.setLabel(
"bottom",
'<span style="color: red; font-size: 18px">Time (min)</span>'
)
</code></pre>
</div>
<p>This time, you've passed the styles in a <code>span</code> HTML tag with appropriate CSS styles. In either case, your plot will look something like this:</p>
<p><img alt="PyQtGraph plot with axis labels" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis labels.</em></p>
<p>Having axis labels highly improves the readability of your plots as you can see in the above example. So, it's a good practice that you should keep in mind when creating your plots.</p>
<h3 id="plot-legends">Plot Legends</h3>
<p>In addition to the axis labels and the plot title, you will often want to show a legend identifying what a given line represents. This feature is particularly important when you start adding multiple lines to a plot.</p>
<p>You can add a legend to a plot by calling the <code>addLegend()</code> method on the <code>PlotWidget</code> object. However, for this method to work, you need to provide a name for each line when calling <code>plot()</code>.</p>
<p>The example below assigns the name "Temperature Sensor" to the <code>plot()</code> method. This name will be used to identify the line in the legend:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.addLegend()
# ...
self.plot_graph.plot(
time,
temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
</code></pre>
</div>
<p>Note that you must call <code>addLegend()</code> before you call <code>plot()</code> for the legend to show up. Otherwise, the plot won't show the legend at all. Now your plot will look like the following:</p>
<p><img alt="PyQtGraph plot with legend" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with legend.</em></p>
<p>The legend appears in the top left by default. If you would like to move it, you can drag and drop the legend elsewhere. You can also specify a default offset by passing a 2-value tuple to the <code>offset</code> parameter when calling the <code>addLegend()</code> method. This will allow you to specify a custom position for the legend.</p>
<h3 id="background-grid">Background Grid</h3>
<p>Adding a background grid can make your plots easier to read, particularly when you're trying to compare relative values against each other. You can turn on the background grid for your plot by calling the <code>showGrid()</code> method on your <code>PlotWidget</code> instance. The method takes two Boolean arguments, <code>x</code> and <code>y</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.showGrid(x=True, y=True)
</code></pre>
</div>
<p>In this call to the <code>showGrid()</code> method, you enable the grid lines in both dimensions <code>x</code> and <code>y</code>. Here's how the plot looks now:</p>
<p><img alt="PyQtGraph plot with grid" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with grid.</em></p>
<p>You can toggle the <code>x</code> and <code>y</code> arguments independently, according to the dimension on which you want to enable the grid lines.</p>
<h3 id="axis-range">Axis Range</h3>
<p>Sometimes, it can be useful to predefine the range of values that is visible on the plot or to lock the axis to a consistent range regardless of the data input. In PyQtGraph, you can do this using the <code>setXRange()</code> and <code>setYRange()</code> methods. They force the plot to only show data within the specified ranges.</p>
<p>Below, we set two ranges, one on each axis. The first argument is the minimum value, and the second is the maximum:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
</code></pre>
</div>
<p>The first line of code sets the x-axis to show values between 1 and 10. The second line sets the y-axis to display values between 20 and 40. Here's how this changes the plot:</p>
<p><img alt="PyQtGraph plot with axis ranges" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis ranges</em></p>
<p>Now your plot looks more consistent. The axis show fix scales that are specifically set for the possible range of input data.</p>
<h2 id="multiple-plot-lines">Multiple Plot Lines</h2>
<p>It is common to have plots that involve more than one dependent variable. In PyQtGraph, you can plot multiple variables in a single chart by calling <code>.plot()</code> multiple times on the same <code>PlotWidget</code> instance.</p>
<p>In the following example, we plot temperatures values from two different sensors. We use the same line style but change the line color. To avoid code repetition, we define a new <code>plot_line()</code> method on our window:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature_1 = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
temperature_2 = [32, 35, 40, 22, 38, 32, 27, 38, 32, 38]
pen = pg.mkPen(color=(255, 0, 0))
self.plot_line("Temperature Sensor 1", minutes, temperature_1, pen, "b")
pen = pg.mkPen(color=(0, 0, 255))
self.plot_line("Temperature Sensor 2", minutes, temperature_2, pen, "r")
def plot_line(self, name, minutes, temperature, pen, brush):
self.plot_graph.plot(
minutes,
temperature,
name=name,
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush=brush,
)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The custom <code>plot_line()</code> method on the main window does the hard work. It accepts a <code>name</code> to set the line name for the plot legend. Then it takes the <code>time</code> and <code>temperature</code> arguments. The <code>pen</code> and <code>brush</code> arguments allow you to tweak other features of the lines.</p>
<p>To plot separate temperature values, we'll create a new list called <code>temperature_2</code> and populate it with random numbers similar to our old <code>temperature</code>, which now is <code>temperature_1</code>. Here's the plot looks now:</p>
<p><img alt="PyQtGrap plot with two lines" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGrap plot with two lines.</em></p>
<p>You can play around with the <code>plot_line()</code> method, customizing the markers, line widths, colors, and other parameters.</p>
<h2 id="creating-dynamic-plots">Creating Dynamic Plots</h2>
<p>You can also create dynamic plots with PyQtGraph. The <code>PlotWidget</code> can take new data and update the plot in real time without affecting other elements. To update a plot dynamically, we need a reference to the line object that the <code>plot()</code> method returns.</p>
<p>Once we have the reference to the plot line, we can call the <code>setData()</code> method on the line object to apply the new data. In the example below, we've adapted our temperature vs time plot to accept new temperature measures every minute. Note that we've set the timer to 300 milliseconds so that we don't have to wait an entire minute to see the updates:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from random import randint
from PyQt6 import QtCore, QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time dynamic plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setYRange(20, 40)
self.time = list(range(10))
self.temperature = [randint(20, 40) for _ in range(10)]
# Get a line reference
self.line = self.plot_graph.plot(
self.time,
self.temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
# Add a timer to simulate new temperature measurements
self.timer = QtCore.QTimer()
self.timer.setInterval(300)
self.timer.timeout.connect(self.update_plot)
self.timer.start()
def update_plot(self):
self.time = self.time[1:]
self.time.append(self.time[-1] + 1)
self.temperature = self.temperature[1:]
self.temperature.append(randint(20, 40))
self.line.setData(self.time, self.temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The first step to creating a dynamic plot is to get a reference to the plot line. In this example, we've used a <code>QTimer</code> object to set the measuring interval. We've connected the <code>update_plot()</code> method with the timer's <code>timeout</code> signal.</p>
<p>The<code>update_plot()</code> method does the work of updating the data in every interval. If you run the app, then you will see a plot with random data scrolling to the left:</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880128994?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>The time scale in the x-axis changes as the stream of data provides new values. You can replace the random data with your own real data. You can take the data from a live sensor readout, API, or from any other stream of data. PyQtGraph is performant enough to support multiple simultaneous dynamic plots using this technique.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, you've learned how to draw basic plots with PyQtGraph and customize plot components, such as lines, markers, titles, axis labels, and more. For a complete overview of PyQtGraph methods and capabilities, see the PyQtGraph <a href="https://pyqtgraph.readthedocs.io/en/latest/">documentation</a>. The PyQtGraph <a href="https://github.com/pyqtgraph/pyqtgraph">repository on Github</a> also has a complete set of plot <a href="https://github.com/pyqtgraph/pyqtgraph/tree/master/pyqtgraph/examples">examples</a>.</p><p>One of the major fields where Python shines is in data science. For data exploration and cleaning, Python has many powerful tools, such as <a href="https://pandas.pydata.org/">pandas</a> and <a href="https://pypi.org/project/polar/">polar</a>. For visualization, Python has Matplotlib.</p>
<p>When you're building GUI applications with PyQt, you can have access to all those tools directly from within your app. While it is possible to embed <code>matplotlib</code> plots in PyQt, the experience doesn't feel entirely <em>native</em>. So, for highly integrated plots, you may want to consider using the <a href="https://www.pyqtgraph.org/">PyQtGraph</a> library instead.</p>
<p>PyQtGraph is built on top of Qt's native <code>QGraphicsScene</code>, so it gives better drawing performance, particularly for live data. It also provides interactivity and the ability to customize plots according to your needs.</p>
<p>In this tutorial, you'll learn the basics of creating plots with PyQtGraph. You'll also explore the different plot customization options, including background color, line colors, line type, axis labels, and more.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#installing-pyqtgraph">Installing PyQtGraph</a></li>
<li><a href="#creating-a-plotwidget-instance">Creating a PlotWidget Instance</a></li>
<li><a href="#customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</a><ul>
<li><a href="#background-color">Background Color</a></li>
<li><a href="#line-color-width-and-style">Line Color, Width, and Style</a></li>
<li><a href="#line-markers">Line Markers</a></li>
<li><a href="#plot-titles">Plot Titles</a></li>
<li><a href="#axis-labels">Axis Labels</a></li>
<li><a href="#plot-legends">Plot Legends</a></li>
<li><a href="#background-grid">Background Grid</a></li>
<li><a href="#axis-range">Axis Range</a></li>
</ul>
</li>
<li><a href="#multiple-plot-lines">Multiple Plot Lines</a></li>
<li><a href="#creating-dynamic-plots">Creating Dynamic Plots</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="installing-pyqtgraph">Installing PyQtGraph</h2>
<p>To use PyQtGraph with PyQt6, you first need to install the library in your Python environment. You can do this using <code>pip</code> as follows:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m pip install pyqtgraph
</code></pre>
</div>
<p>Once the installation is complete, you will be able to import the module into your Python code. So, now you are ready to start creating plots.</p>
<h2 id="creating-a-plotwidget-instance">Creating a <code>PlotWidget</code> Instance</h2>
<p>In PyQtGraph, all plots use the <a href="https://pyqtgraph.readthedocs.io/en/latest/api_reference/widgets/plotwidget.html"><code>PlotWidget</code></a> class. This widget provides a <em>canvas</em> on which we can add and configure many types of plots. Under the hood, <code>PlotWidget</code> uses Qt's <code>QGraphicsScene</code> class, meaning that it's fast, efficient, and well-integrated with the rest of your app.</p>
<p>The code below shows a basic GUI app with a single <code>PlotWidget</code> in a <code>QMainWindow</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt6 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(minutes, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>In this short example, you create a PyQt app with a <code>PlotWidget</code> as its central widget. Then you create two lists of sample data for time and temperature. The final step to create the plot is to call the <code>plot()</code> methods with the data you want to visualize.</p>
<p>The first argument to <code>plot()</code> will be your <code>x</code> coordinate, while the second argument will be the <code>y</code> coordinate.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In all the examples in this tutorial, we import PyQtGraph using <code>import pyqtgraph as pg</code>. This is a common practice in PyQtGraph examples to keep things tidy and reduce typing.</p>
<p>If you run the above application, then you'll get the following window on your screen:</p>
<p><img alt="Basic PyQtGraph plot: Temperature vs time" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>Basic PyQtGraph plot: Temperature vs time.</em></p>
<p>PyQtGraph's default plot style is quite basic — a black background with a thin (barely visible) white line. Fortunately, the library provides several options that will allow us to deeply customize our plots.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In the examples in this tutorial, we'll create the PyQtGraph widget in code. To learn how to embed PyQtGraph plots when using Qt Designer, check out <a href="https://www.pythonguis.com/tutorials/embed-pyqtgraph-custom-widgets-qt-app/">Embedding custom widgets from Qt Designer</a>.</p>
<p>In the following section, we'll learn about the options we have available in PyQtGraph to improve the appearance and usability of our plots.</p>
<h2 id="customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</h2>
<p>Because PyQtGraph uses Qt's <code>QGraphicsScene</code> to render the graphs, we have access to all the standard Qt line and shape styling options for use in plots. PyQtGraph provides an <a href="https://en.wikipedia.org/wiki/API">API</a> for using these options to draw plots and manage the plot canvas.</p>
<p>Below, we'll explore the most common styling features that you'll need to create and customize your own plots with PyQtGraph.</p>
<h3 id="background-color">Background Color</h3>
<p>Beginning with the app skeleton above, we can change the background color by calling <code>setBackground()</code> on our <code>PlotWidget</code> instance, <code>self.graphWidget</code>. The code below sets the background to white by passing in the string <code>"w"</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(minutes, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Calling <code>setBackground()</code> with <code>"w"</code> as an argument changes the background of your plot to white, as you can see in the following window:</p>
<p><img alt="PyQtGraph plot with a white background" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a white background.</em></p>
<p>There are a number of colors available using single letters, as we did in the example above. They're based on the standard colors used in Matplotlib. Here are the most common codes:</p>
<table>
<thead>
<tr>
<th>Letter Code</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"b"</code></td>
<td>Blue</td>
</tr>
<tr>
<td><code>"c"</code></td>
<td>Cian</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Grey</td>
</tr>
<tr>
<td><code>"g"</code></td>
<td>Green</td>
</tr>
<tr>
<td><code>"k"</code></td>
<td>Black</td>
</tr>
<tr>
<td><code>"m"</code></td>
<td>Magenta</td>
</tr>
<tr>
<td><code>"r"</code></td>
<td>Red</td>
</tr>
<tr>
<td><code>"w"</code></td>
<td>White</td>
</tr>
<tr>
<td><code>"y"</code></td>
<td>Yellow</td>
</tr>
</tbody>
</table>
<p>In addition to these single-letter codes, we can create custom colors using the <a href="https://en.wikipedia.org/wiki/Web_colors#Hex_triplet">hexadecimal notation</a> as a string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground("#bbccaa") # Hex
</code></pre>
</div>
<p>We can also use <a href="https://en.wikipedia.org/wiki/RGB_color_model">RGB</a> and <a href="https://en.wikipedia.org/wiki/RGBA_color_model">RGBA</a> values passed in as 3-value and 4-value tuples, respectively. We must use values in the range from 0 to 255:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground((100, 50, 255)) # RGB each 0-255
self.plot_graph.setBackground((100, 50, 255, 25)) # RGBA (A = alpha opacity)
</code></pre>
</div>
<p>The first call to <code>setBackground()</code> takes a tuple representing an RGB color, while the second call takes a tuple representing an RGBA color.</p>
<p>We can also specify colors using Qt's <code>QColor</code> class if we prefer it:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtGui
# ...
self.plot_graph.setBackground(QtGui.QColor(100, 50, 254, 25))
</code></pre>
</div>
<p>Using <code>QColor</code> can be useful when you're using specific <code>QColor</code> objects elsewhere in your application and want to reuse them in your plots. For example, say that your app has a custom window background color, and you want to use it in the plots as well. Then you can do something like the following:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">color = self.palette().color(QtGui.QPalette.Window)
# ...
self.plot_graph.setBackground(color)
</code></pre>
</div>
<p>In the first line, you get the GUI's background color, while in the second line, you use that color for your plots.</p>
<h3 id="line-color-width-and-style">Line Color, Width, and Style</h3>
<p>Plot lines in PyQtGraph are drawn using the Qt <code>QPen</code> class. This gives us full control over line drawing, as we would have in any other <code>QGraphicsScene</code> drawing. To use a custom pen, you need to create a new <code>QPen</code> instance and pass it into the <code>plot()</code> method.</p>
<p>In the app below, we use a custom <code>QPen</code> object to change the line color to red:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature, pen=pen)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Here, we create a <code>QPen</code> object, passing in a 3-value tuple that defines an RGB red color. We could also define this color with the <code>"r"</code> code or with a <code>QColor</code> object. Then, we pass the pen to <code>plot()</code> with the <code>pen</code> argument.</p>
<p><img alt="PyQtGraph plot with a red plot line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red plot line.</em></p>
<p>By tweaking the <code>QPen</code> object, we can change the appearance of the line. For example, you can change the line width in pixels and the style (dashed, dotted, etc.), using Qt's line styles.</p>
<p>Update the following lines of code in your app to create a red, dashed line with 5 pixels of width:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtCore, QtWidgets
# ...
pen = pg.mkPen(color=(255, 0, 0), width=5, style=QtCore.Qt.DashLine)
</code></pre>
</div>
<p>The result of this code is shown below, giving a 5-pixel, dashed, red line:</p>
<p><img alt="PyQtGraph plot with a red, dashed, and 5-pixel line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red, dashed, and 5-pixel line</em></p>
<p>You can use all other Qt's line styles, including <code>Qt.SolidLine</code>, <code>Qt.DotLine</code>, <code>Qt.DashDotLine</code>, and <code>Qt.DashDotDotLine</code>. Examples of each of these lines are shown in the image below:</p>
<p><img alt="Qt Line Types" src="https://www.pythonguis.com/tutorials/pyqt6-plotting-pyqtgraph/Qt_Line_Types.png"/>
<em>Qt's line styles.</em></p>
<p>To learn more about Qt's line styles, check the <a href="https://doc.qt.io/qt-5/qpen.html#pen-style">documentation</a> about pen styles. There, you'll all you need to deeply customize the lines in your PyQtGraph plots.</p>
<h3 id="line-markers">Line Markers</h3>
<p>For many plots, it can be helpful to use point markers in addition or instead of lines on the plot. To draw a marker on your plot, pass the symbol you want to use as a marker when calling <code>plot()</code>. The following example uses the plus sign as a marker:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.plot(hour, temperature, symbol="+")
</code></pre>
</div>
<p>In this line of code, you pass a plus sign to the <code>symbol</code> argument. This tells PyQtGraph to use that symbol as a marker for the points in your plot.</p>
<p>If you use a custom <code>symbol</code>, then you can also use the <code>symbolSize</code>, <code>symbolBrush</code>, and <code>symbolPen</code> arguments to further customize the marker.</p>
<p>The value passed as <code>symbolBrush</code> can be any color, or <code>QBrush</code> instance, while <code>symbolPen</code> can be any color or a <code>QPen</code> instance. The pen is used to draw the shape, while the brush is used for the fill.</p>
<p>Go ahead and update your app's code to use a blue marker of size 15, on a red line:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.plot(
time,
temperature,
pen=pen,
symbol="+",
symbolSize=20,
symbolBrush="b",
)
</code></pre>
</div>
<p>In this code, you pass a plus sign to the <code>symbol</code> argument. You also customize the marker size and color. The resulting plot looks something like this:</p>
<p><img alt="PyQtGraph plot with a plus sign as a point marker" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a plus sign as a point marker.</em></p>
<p>In addition to the <code>+</code> plot marker, PyQtGraph supports the markers shown in the table below:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>Marker Shape</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"o"</code></td>
<td>Circle</td>
</tr>
<tr>
<td><code>"s"</code></td>
<td>Square</td>
</tr>
<tr>
<td><code>"t"</code></td>
<td>Triangle</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Diamond</td>
</tr>
<tr>
<td><code>"+"</code></td>
<td>Plus</td>
</tr>
<tr>
<td><code>"t1"</code></td>
<td>Triangle pointing upwards</td>
</tr>
<tr>
<td><code>"t2"</code></td>
<td>Triangle pointing right side</td>
</tr>
<tr>
<td><code>"t3"</code></td>
<td>Triangle pointing left side</td>
</tr>
<tr>
<td><code>"p"</code></td>
<td>Pentagon</td>
</tr>
<tr>
<td><code>"h"</code></td>
<td>Hexagon</td>
</tr>
<tr>
<td><code>"star"</code></td>
<td>Star</td>
</tr>
<tr>
<td><code>"x"</code></td>
<td>Cross</td>
</tr>
<tr>
<td><code>"arrow_up"</code></td>
<td>Arrow Up</td>
</tr>
<tr>
<td><code>"arrow_right"</code></td>
<td>Arrow Right</td>
</tr>
<tr>
<td><code>"arrow_down"</code></td>
<td>Arrow Down</td>
</tr>
<tr>
<td><code>"arrow_left"</code></td>
<td>Arrow Left</td>
</tr>
<tr>
<td><code>"crosshair"</code></td>
<td>Crosshair</td>
</tr>
</tbody>
</table>
<p>You can use any of these symbols as markers for your data points. If you have more specific marker requirements, then you can also use a <code>QPainterPath</code> object, which allows you to draw completely custom marker shapes.</p>
<h3 id="plot-titles">Plot Titles</h3>
<p>Plot titles are important to provide context around what is shown on a given chart. In PyQtGraph, you can add a main plot title using the <code>setTitle()</code> method on the <code>PlotWidget</code> object. Your title can be a regular Python string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time")
</code></pre>
</div>
<p>You can style your titles and change their font color and size by passing additional arguments to <code>setTitle()</code>. The code below sets the color to blue and the font size to 20 points:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
</code></pre>
</div>
<p>In this line of code, you set the title's font color to blue and the size to 20 points using the <code>color</code> and <code>size</code> arguments of <code>setTitle()</code>.</p>
<p>You could've even used CSS style and basic HTML tag syntax if you prefer, although it's less readable:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle(
'<span style="color: blue; font-size: 20pt">Temperature vs Time</span>'
)
</code></pre>
</div>
<p>In this case, you use a <code>span</code> HTML tag to wrap the title and apply some CSS styles on to of it. The final result is the same as suing the <code>color</code> and <code>size</code> arguments. Your plot will look like this:</p>
<p><img alt="PyQtGraph plot with title" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with title.</em></p>
<p>Your plot looks way better now. You can continue customizing it by adding informative lables to both axis.</p>
<h3 id="axis-labels">Axis Labels</h3>
<p>When it comes to axis labels, we can use the <code>setLabel()</code> method to create them. This method requires two arguments, <code>position</code> and <code>text</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel("left", "Temperature (°C)")
self.plot_graph.setLabel("bottom", "Time (min)")
</code></pre>
</div>
<p>The <code>position</code> argument can be any one of <code>"left"</code>, <code>"right"</code>, <code>"top"</code>, or <code>"bottom"</code>. They define the position of the axis on which the text is placed. The second argument, <code>text</code> is the text you want to use for the label.</p>
<p>You can pass an optional <code>style</code> argument into the <code>setLabel()</code> method. In this case, you need to use valid CSS name-value pairs. To provide these CSS pairs, you can use a dictionary:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
</code></pre>
</div>
<p>Here, you first create a dictionary containing CSS pairs. Then you pass this dictionary as an argument to the <code>setLabel()</code> method. Note that you need to use the dictionary unpacking operator to unpack the styles in the method call.</p>
<p>Again, you can use basic HTML syntax and CSS for the labels if you prefer:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel(
"left",
'<span style="color: red; font-size: 18px">Temperature (°C)</span>'
)
self.plot_graph.setLabel(
"bottom",
'<span style="color: red; font-size: 18px">Time (min)</span>'
)
</code></pre>
</div>
<p>This time, you've passed the styles in a <code>span</code> HTML tag with appropriate CSS styles. In either case, your plot will look something like this:</p>
<p><img alt="PyQtGraph plot with axis labels" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis labels.</em></p>
<p>Having axis labels highly improves the readability of your plots as you can see in the above example. So, it's a good practice that you should keep in mind when creating your plots.</p>
<h3 id="plot-legends">Plot Legends</h3>
<p>In addition to the axis labels and the plot title, you will often want to show a legend identifying what a given line represents. This feature is particularly important when you start adding multiple lines to a plot.</p>
<p>You can add a legend to a plot by calling the <code>addLegend()</code> method on the <code>PlotWidget</code> object. However, for this method to work, you need to provide a name for each line when calling <code>plot()</code>.</p>
<p>The example below assigns the name "Temperature Sensor" to the <code>plot()</code> method. This name will be used to identify the line in the legend:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.addLegend()
# ...
self.plot_graph.plot(
time,
temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
</code></pre>
</div>
<p>Note that you must call <code>addLegend()</code> before you call <code>plot()</code> for the legend to show up. Otherwise, the plot won't show the legend at all. Now your plot will look like the following:</p>
<p><img alt="PyQtGraph plot with legend" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with legend.</em></p>
<p>The legend appears in the top left by default. If you would like to move it, you can drag and drop the legend elsewhere. You can also specify a default offset by passing a 2-value tuple to the <code>offset</code> parameter when calling the <code>addLegend()</code> method. This will allow you to specify a custom position for the legend.</p>
<h3 id="background-grid">Background Grid</h3>
<p>Adding a background grid can make your plots easier to read, particularly when you're trying to compare relative values against each other. You can turn on the background grid for your plot by calling the <code>showGrid()</code> method on your <code>PlotWidget</code> instance. The method takes two Boolean arguments, <code>x</code> and <code>y</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.showGrid(x=True, y=True)
</code></pre>
</div>
<p>In this call to the <code>showGrid()</code> method, you enable the grid lines in both dimensions <code>x</code> and <code>y</code>. Here's how the plot looks now:</p>
<p><img alt="PyQtGraph plot with grid" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with grid.</em></p>
<p>You can toggle the <code>x</code> and <code>y</code> arguments independently, according to the dimension on which you want to enable the grid lines.</p>
<h3 id="axis-range">Axis Range</h3>
<p>Sometimes, it can be useful to predefine the range of values that is visible on the plot or to lock the axis to a consistent range regardless of the data input. In PyQtGraph, you can do this using the <code>setXRange()</code> and <code>setYRange()</code> methods. They force the plot to only show data within the specified ranges.</p>
<p>Below, we set two ranges, one on each axis. The first argument is the minimum value, and the second is the maximum:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
</code></pre>
</div>
<p>The first line of code sets the x-axis to show values between 1 and 10. The second line sets the y-axis to display values between 20 and 40. Here's how this changes the plot:</p>
<p><img alt="PyQtGraph plot with axis ranges" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis ranges</em></p>
<p>Now your plot looks more consistent. The axis show fix scales that are specifically set for the possible range of input data.</p>
<h2 id="multiple-plot-lines">Multiple Plot Lines</h2>
<p>It is common to have plots that involve more than one dependent variable. In PyQtGraph, you can plot multiple variables in a single chart by calling <code>.plot()</code> multiple times on the same <code>PlotWidget</code> instance.</p>
<p>In the following example, we plot temperatures values from two different sensors. We use the same line style but change the line color. To avoid code repetition, we define a new <code>plot_line()</code> method on our window:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
minutes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature_1 = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
temperature_2 = [32, 35, 40, 22, 38, 32, 27, 38, 32, 38]
pen = pg.mkPen(color=(255, 0, 0))
self.plot_line("Temperature Sensor 1", minutes, temperature_1, pen, "b")
pen = pg.mkPen(color=(0, 0, 255))
self.plot_line("Temperature Sensor 2", minutes, temperature_2, pen, "r")
def plot_line(self, name, minutes, temperature, pen, brush):
self.plot_graph.plot(
minutes,
temperature,
name=name,
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush=brush,
)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The custom <code>plot_line()</code> method on the main window does the hard work. It accepts a <code>name</code> to set the line name for the plot legend. Then it takes the <code>time</code> and <code>temperature</code> arguments. The <code>pen</code> and <code>brush</code> arguments allow you to tweak other features of the lines.</p>
<p>To plot separate temperature values, we'll create a new list called <code>temperature_2</code> and populate it with random numbers similar to our old <code>temperature</code>, which now is <code>temperature_1</code>. Here's the plot looks now:</p>
<p><img alt="PyQtGrap plot with two lines" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGrap plot with two lines.</em></p>
<p>You can play around with the <code>plot_line()</code> method, customizing the markers, line widths, colors, and other parameters.</p>
<h2 id="creating-dynamic-plots">Creating Dynamic Plots</h2>
<p>You can also create dynamic plots with PyQtGraph. The <code>PlotWidget</code> can take new data and update the plot in real time without affecting other elements. To update a plot dynamically, we need a reference to the line object that the <code>plot()</code> method returns.</p>
<p>Once we have the reference to the plot line, we can call the <code>setData()</code> method on the line object to apply the new data. In the example below, we've adapted our temperature vs time plot to accept new temperature measures every minute. Note that we've set the timer to 300 milliseconds so that we don't have to wait an entire minute to see the updates:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from random import randint
from PyQt6 import QtCore, QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time dynamic plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setYRange(20, 40)
self.time = list(range(10))
self.temperature = [randint(20, 40) for _ in range(10)]
# Get a line reference
self.line = self.plot_graph.plot(
self.time,
self.temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
# Add a timer to simulate new temperature measurements
self.timer = QtCore.QTimer()
self.timer.setInterval(300)
self.timer.timeout.connect(self.update_plot)
self.timer.start()
def update_plot(self):
self.time = self.time[1:]
self.time.append(self.time[-1] + 1)
self.temperature = self.temperature[1:]
self.temperature.append(randint(20, 40))
self.line.setData(self.time, self.temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The first step to creating a dynamic plot is to get a reference to the plot line. In this example, we've used a <code>QTimer</code> object to set the measuring interval. We've connected the <code>update_plot()</code> method with the timer's <code>timeout</code> signal.</p>
<p>The<code>update_plot()</code> method does the work of updating the data in every interval. If you run the app, then you will see a plot with random data scrolling to the left:</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880128994?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>The time scale in the x-axis changes as the stream of data provides new values. You can replace the random data with your own real data. You can take the data from a live sensor readout, API, or from any other stream of data. PyQtGraph is performant enough to support multiple simultaneous dynamic plots using this technique.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, you've learned how to draw basic plots with PyQtGraph and customize plot components, such as lines, markers, titles, axis labels, and more. For a complete overview of PyQtGraph methods and capabilities, see the PyQtGraph <a href="https://pyqtgraph.readthedocs.io/en/latest/">documentation</a>. The PyQtGraph <a href="https://github.com/pyqtgraph/pyqtgraph">repository on Github</a> also has a complete set of plot <a href="https://github.com/pyqtgraph/pyqtgraph/tree/master/pyqtgraph/examples">examples</a>.</p>Q&A: How Do I Display Images in PyQt6? — Using QLabel to easily add images to your applications2024-02-14T06:00:00+00:002024-02-14T06:00:00+00:00John Limtag:www.pythonguis.com,2024-02-14:/faq/adding-images-to-pyqt6-applications/<p>Adding images to your application is a common requirement, whether you're building an image/photo viewer, or just want to add some decoration to your GUI. Unfortunately, because of how this is done in Qt, it can be a little bit tricky to work out at first.</p>
<p>In this short tutorial, we will look at how you can insert an external image into your PyQt6 application layout, using both code and Qt Designer.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#which-widget-to-use">Which widget to use?</a></li>
<li><a href="#using-qt-designer">Using Qt Designer</a></li>
<li><a href="#using-code">Using Code</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h3 id="which-widget-to-use">Which widget to use?</h3>
<p>Since you're wanting to insert an image you might be expecting to use a widget named <code>QImage</code> or similar, but that would make a bit too much sense! <code>QImage</code> is actually Qt's image <em>object</em> type, which is used to store the actual image data for use within your application. The <em>widget</em> you use to display an image is <code>QLabel</code>.</p>
<p>The primary use of <code>QLabel</code> is of course to add labels to a UI, but it also has the ability to display an image — or <em>pixmap</em> — instead, covering the entire area of the widget. Below we'll look at how to use <code>QLabel</code> to display a widget in your applications.</p>
<h3 id="using-qt-designer">Using Qt Designer</h3>
<p>First, create a <em>MainWindow</em> object in Qt Designer and add a "Label" to it. You can find Label at in <em>Display Widgets</em> in the bottom of the left hand panel. Drag this onto the <code>QMainWindow</code> to add it.</p>
<p><img alt="MainWindow with a single QLabel added" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/1.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-600 600w" loading="lazy" width="1917" height="1027"/>
<em>MainWindow with a single QLabel added</em></p>
<p>Next, with the Label selected, look in the right hand <code>QLabel</code> properties panel for the <code>pixmap</code> property (scroll down to the blue region). From the property editor dropdown select "Choose File…" and select an image file to insert.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442141?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>As you can see, the image is inserted, but the image is kept at its original size, cropped to the boundaries of the<code>QLabel</code> box. You need to resize the <code>QLabel</code> to be able to see the entire image.</p>
<p>In the same controls panel, click to enable <code>scaledContents</code>.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442184?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>When <code>scaledContents</code> is enabled the image is resized to the fit the bounding box of the <code>QLabel</code> widget. This shows the entire image at all times, although it does not respect the aspect ratio of the image if you resize the widget.</p>
<p>You can now save your UI to file (e.g. as <code>mainwindow.ui</code>).</p>
<p>To view the resulting UI, we can use the standard application template below. This loads the <code>.ui</code> file we've created (<code>mainwindow.ui</code>) creates the window and starts up the application.</p>
<div class="code-block">
<span class="code-block-language code-block-PyQt6">PyQt6</span>
<pre><code class="PyQt6">import sys
from PyQt6 import QtWidgets, uic
app = QtWidgets.QApplication(sys.argv)
window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()
</code></pre>
</div>
<p>Running the above code will create a window, with the image displayed in the middle.</p>
<p><img alt="QtDesigner application showing a Cat" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/5.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-600 600w" loading="lazy" width="802" height="639"/>
<em>QtDesigner application showing a Cat</em></p>
<h3 id="using-code">Using Code</h3>
<p>Instead of using Qt Designer, you might also want to show an image in your application through code. As before we use a <code>QLabel</code> widget and add a <em>pixmap</em> image to it. This is done using the <code>QLabel</code> method <code>.setPixmap()</code>. The full code is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="d01cde149eed43719067203dbd5aa30f" v-on:click="switch_tab">PyQt6</li>
<li class="tab-link" data-tab="a178338ab272490e95c0b3b7e994c51e" v-on:click="switch_tab">PySide6</li>
<li class="tab-link" data-tab="0d6501c1c8b843be9c4e784c97272753" v-on:click="switch_tab">PyQt5</li>
<li class="tab-link" data-tab="a9abb5d91a8342a794f4851c64bbaca4" v-on:click="switch_tab">PySide2</li></ul><div class="tab-content current code-block-outer" id="d01cde149eed43719067203dbd5aa30f">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a178338ab272490e95c0b3b7e994c51e">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="0d6501c1c8b843be9c4e784c97272753">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a9abb5d91a8342a794f4851c64bbaca4">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
</code></pre>
</div>
</div>
</div>
<p>The block of code below shows the process of creating the <code>QLabel</code>, creating a <code>QPixmap</code> object from our file <code>cat.jpg</code> (passed as a file path), setting this <code>QPixmap</code> onto the <code>QLabel</code> with <code>.setPixmap()</code> and then finally resizing the window to fit the image.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p>Launching this code will show a window with the cat photo displayed and the window sized to the size of the image.</p>
<p><img alt="QMainWindow with Cat image displayed" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/4.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-600 600w" loading="lazy" width="602" height="439"/>
<em>QMainWindow with Cat image displayed</em></p>
<p>Just as in Qt designer, you can call <code>.setScaledContents(True)</code> on your <code>QLabel</code> image to enable scaled mode, which resizes the image to fit the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
label.setScaledContents(True)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Notice that you set the scaled state on the <code>QLabel</code> widget and not the image pixmap itself.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In this quick tutorial we've covered how to insert images into your Qt UIs using <code>QLabel</code> both from Qt Designer and directly from PyQt5/PySide2 code.</p><p>Adding images to your application is a common requirement, whether you're building an image/photo viewer, or just want to add some decoration to your GUI. Unfortunately, because of how this is done in Qt, it can be a little bit tricky to work out at first.</p>
<p>In this short tutorial, we will look at how you can insert an external image into your PyQt6 application layout, using both code and Qt Designer.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#which-widget-to-use">Which widget to use?</a></li>
<li><a href="#using-qt-designer">Using Qt Designer</a></li>
<li><a href="#using-code">Using Code</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h3 id="which-widget-to-use">Which widget to use?</h3>
<p>Since you're wanting to insert an image you might be expecting to use a widget named <code>QImage</code> or similar, but that would make a bit too much sense! <code>QImage</code> is actually Qt's image <em>object</em> type, which is used to store the actual image data for use within your application. The <em>widget</em> you use to display an image is <code>QLabel</code>.</p>
<p>The primary use of <code>QLabel</code> is of course to add labels to a UI, but it also has the ability to display an image — or <em>pixmap</em> — instead, covering the entire area of the widget. Below we'll look at how to use <code>QLabel</code> to display a widget in your applications.</p>
<h3 id="using-qt-designer">Using Qt Designer</h3>
<p>First, create a <em>MainWindow</em> object in Qt Designer and add a "Label" to it. You can find Label at in <em>Display Widgets</em> in the bottom of the left hand panel. Drag this onto the <code>QMainWindow</code> to add it.</p>
<p><img alt="MainWindow with a single QLabel added" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/1.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/1.png?tr=w-600 600w" loading="lazy" width="1917" height="1027"/>
<em>MainWindow with a single QLabel added</em></p>
<p>Next, with the Label selected, look in the right hand <code>QLabel</code> properties panel for the <code>pixmap</code> property (scroll down to the blue region). From the property editor dropdown select "Choose File…" and select an image file to insert.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442141?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>As you can see, the image is inserted, but the image is kept at its original size, cropped to the boundaries of the<code>QLabel</code> box. You need to resize the <code>QLabel</code> to be able to see the entire image.</p>
<p>In the same controls panel, click to enable <code>scaledContents</code>.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880442184?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>When <code>scaledContents</code> is enabled the image is resized to the fit the bounding box of the <code>QLabel</code> widget. This shows the entire image at all times, although it does not respect the aspect ratio of the image if you resize the widget.</p>
<p>You can now save your UI to file (e.g. as <code>mainwindow.ui</code>).</p>
<p>To view the resulting UI, we can use the standard application template below. This loads the <code>.ui</code> file we've created (<code>mainwindow.ui</code>) creates the window and starts up the application.</p>
<div class="code-block">
<span class="code-block-language code-block-PyQt6">PyQt6</span>
<pre><code class="PyQt6">import sys
from PyQt6 import QtWidgets, uic
app = QtWidgets.QApplication(sys.argv)
window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()
</code></pre>
</div>
<p>Running the above code will create a window, with the image displayed in the middle.</p>
<p><img alt="QtDesigner application showing a Cat" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/5.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/5.png?tr=w-600 600w" loading="lazy" width="802" height="639"/>
<em>QtDesigner application showing a Cat</em></p>
<h3 id="using-code">Using Code</h3>
<p>Instead of using Qt Designer, you might also want to show an image in your application through code. As before we use a <code>QLabel</code> widget and add a <em>pixmap</em> image to it. This is done using the <code>QLabel</code> method <code>.setPixmap()</code>. The full code is shown below.</p>
<div class="tabbed-area multicode"><ul class="tabs"><li class="tab-link current" data-tab="d01cde149eed43719067203dbd5aa30f" v-on:click="switch_tab">PyQt6</li>
<li class="tab-link" data-tab="a178338ab272490e95c0b3b7e994c51e" v-on:click="switch_tab">PySide6</li>
<li class="tab-link" data-tab="0d6501c1c8b843be9c4e784c97272753" v-on:click="switch_tab">PyQt5</li>
<li class="tab-link" data-tab="a9abb5d91a8342a794f4851c64bbaca4" v-on:click="switch_tab">PySide2</li></ul><div class="tab-content current code-block-outer" id="d01cde149eed43719067203dbd5aa30f">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a178338ab272490e95c0b3b7e994c51e">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="0d6501c1c8b843be9c4e784c97272753">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
</code></pre>
</div>
</div>
<div class="tab-content code-block-outer" id="a9abb5d91a8342a794f4851c64bbaca4">
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import sys
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QMainWindow, QApplication, QLabel
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.title = "Image Viewer"
self.setWindowTitle(self.title)
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
</code></pre>
</div>
</div>
</div>
<p>The block of code below shows the process of creating the <code>QLabel</code>, creating a <code>QPixmap</code> object from our file <code>cat.jpg</code> (passed as a file path), setting this <code>QPixmap</code> onto the <code>QLabel</code> with <code>.setPixmap()</code> and then finally resizing the window to fit the image.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p>Launching this code will show a window with the cat photo displayed and the window sized to the size of the image.</p>
<p><img alt="QMainWindow with Cat image displayed" src="https://www.pythonguis.com/static/faq/adding-images-to-applications/4.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/adding-images-to-applications/4.png?tr=w-600 600w" loading="lazy" width="602" height="439"/>
<em>QMainWindow with Cat image displayed</em></p>
<p>Just as in Qt designer, you can call <code>.setScaledContents(True)</code> on your <code>QLabel</code> image to enable scaled mode, which resizes the image to fit the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
label.setScaledContents(True)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())
</code></pre>
</div>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Notice that you set the scaled state on the <code>QLabel</code> widget and not the image pixmap itself.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In this quick tutorial we've covered how to insert images into your Qt UIs using <code>QLabel</code> both from Qt Designer and directly from PyQt5/PySide2 code.</p>Drag & Drop Widgets with PyQt6 — Sort widgets visually with drag and drop in a container2024-02-07T13:00:00+00:002024-02-07T13:00:00+00:00Martin Fitzpatricktag:www.pythonguis.com,2024-02-07:/faq/pyqt6-drag-drop-widgets/<p>I had an interesting question from a reader of my <a href="https://www.pythonguis.com/pyqt6-book/">PyQt6 book</a>, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.</p>
<blockquote>
<p>I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of <code>QPushButton</code>, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.</p>
</blockquote>
<p>First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#drag-drop-widgets">Drag & Drop Widgets</a></li>
<li><a href="#visual-drag-drop">Visual Drag & Drop</a></li>
<li><a href="#generic-drag-drop-container">Generic Drag & Drop Container</a></li>
<li><a href="#adding-a-visual-drop-target">Adding a Visual Drop Target</a></li>
</ul>
</div>
<h2 id="drag-drop-widgets">Drag & Drop Widgets</h2>
<p>We'll start with a simple application which creates a window using <code>QWidget</code> and places a series of <code>QPushButton</code> widgets into it.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can substitute <code>QPushButton</code> for any other widget you like, e.g. <code>QLabel</code>. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = QPushButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this you should see something like this.</p>
<p><img alt="Widgets in a layout" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/windows-in-layout.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>The series of <code>QPushButton widgets</code> in a horizontal layout.</em></p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Here we're creating a window, but the <code>Window</code> widget is subclassed from <code>QWidget</code>, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.</p>
<p><code>QPushButton</code> objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>We implement a <code>mouseMoveEvent</code> which accepts the single <code>e</code> parameter of the event. We check to see if the <em>left</em> mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a <code>QDrag</code> object, passing in <code>self</code> to give us access later to the widget that was dragged. We also <em>must</em> pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.</p>
<p>Finally, we initiate a drag by calling <code>drag.exec_(Qt.MoveAction)</code>. As with dialogs <code>exec_()</code> starts a new event loop, blocking the main loop until the drag is complete. The parameter <code>Qt.MoveAction</code> tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.</p>
<p>You can update the main window code to use our new <code>DragButton</code> class as follows.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
</code></pre>
</div>
<p>If you run the code now, you <em>can</em> drag the buttons, but you'll notice the drag is forbidden.</p>
<p><img alt="Drag forbidden" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-forbidden.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts but is forbidden.</em></p>
<p>What's happening? The mouse movement is being detected by our <code>DragButton</code> object and the drag started, but the main window does not accept drag & drop.</p>
<p>To fix this we need to enable drops on the window and implement <code>dragEnterEvent</code> to actually accept them.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling <code>drag.exec_()</code>.</p>
<p><img alt="Drag accepted" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-accepted.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts and is accepted, showing a move icon.</em></p>
<p>Releasing the mouse button during a drag drop operation triggers a <code>dropEvent</code> on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our <code>dropEvent</code> method.</p>
<p>The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.</p>
<p>To determine where to place the widget, we iterate over all the widgets in the layout, <em>until</em> we find one who's <code>x</code> position is <em>greater</em> than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.</p>
<p>If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment <code>n</code> one further (in the <code>else:</code> block below).</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x():
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using <code>if pos.x() < w.x() + w.size().width() // 2:</code> -- that is x + half of the width.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The complete working drag-drop code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<h2 id="visual-drag-drop">Visual Drag & Drop</h2>
<p>We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.</p>
<p>Qt's <code>QDrag</code> handler natively provides a mechanism for showing dragged objects which we can use. We can update our <code>DragButton</code> class to pass a <em>pixmap</em> image to <code>QDrag</code> and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a <code>QPixmap</code> of the widget we're dragging.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>To create the pixmap we create a <code>QPixmap</code> object passing in the size of the widget this event is fired on with <code>self.size()</code>. This creates an empty <em>pixmap</em> which we can then pass into <code>self.render</code> to <em>render</em> -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the <code>drag</code> object.</p>
<p>If you run the code with this modification you'll see something like the following --</p>
<p><img alt="Drag visual" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-visual.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget showing the dragged widget.</em></p>
<h2 id="generic-drag-drop-container">Generic Drag & Drop Container</h2>
<p>We now have a working drag and drop behavior implemented on our window.
We can take this a step further and implement a <em>generic</em> drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget <code>DragWidget</code> which can be added to any window.</p>
<p>You can add <em>items</em> -- instances of <code>DragItem</code> -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal <code>orderChanged</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
break
else:
# We aren't on the left hand/upper side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
self.orderChanged.emit(self.get_item_data())
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p><img alt="Generic drag drop horizontal" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-horizontal.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-600 600w" loading="lazy" width="306" height="139"/>
<em>Generic drag-drop sorting in horizontal orientation.</em></p>
<p>You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal <code>QLabel</code> which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call <code>get_item_data</code> yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort <em>anything</em> not just strings.</p>
<p>In the example above we're passing in the enumerated index as the data, so dragging will output (via the <code>print</code> connected to <code>orderChanged</code>) something like:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]
</code></pre>
</div>
<p>If you remove the <code>item.set_data(n)</code> you'll see the labels emitted on changes.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']
</code></pre>
</div>
<p>We've also implemented <em>orientation</em> onto the <code>DragWidget</code> using the Qt built in flags <code>Qt.Orientation.Vertical</code> or <code>Qt.Orientation.Horizontal</code>. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.</p>
<p><img alt="Generic drag drop vertical" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-vertical.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-600 600w" loading="lazy" width="202" height="216"/>
<em>Generic drag-drop sorting in vertical orientation.</em></p>
<h2 id="adding-a-visual-drop-target">Adding a Visual Drop Target</h2>
<p>If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be
inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using <em>guesswork</em> to get it right.</p>
<p>With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.</p>
<p>In this final section we'll implement this type of drag and drop preview indicator.</p>
<p>The first step is to define our target indicator. This is just another label,
which in our example is empty, with custom styles applied to make it have a
solid "shadow" like background. This makes it obviously different to the
items in the list, so it stands out as something distinct.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.</p>
<p>The drag item is unchanged, but we need to implement some additional behavior on our <code>DragWidget</code> to add the target, control showing and moving it.</p>
<p>First we'll add the drag target indicator to the layout on our <code>DragWidget</code>. This is hidden to begin with, but will be shown during the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
</code></pre>
</div>
<p>Next we modify the <code>DragWidget.dragMoveEvent</code> to show the drag target indicator. We show it by <em>inserting</em> it into the layout and then calling <code>.show</code> -- inserting a widget which is already in a layout will move it.
We also hide the original item which is being dragged.</p>
<p>In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.</p>
<p>Instead, the dragged item is left in place and hidden during move.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
</code></pre>
</div>
<p>The method <code>self._find_drop_location</code> finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.</p>
<p>The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
</code></pre>
</div>
<p>The drop location <code>n</code> is returned for use in the <code>dragMoveEvent</code> to place the drop target indicator.</p>
<p>Next wee need to update the <code>get_item_data</code> handler to ignore the drop target indicator. To do this we check <code>w</code> against <code>self._drag_target_indicator</code> and skip if it is the same. With this change the method will work as expected.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
</code></pre>
</div>
<p>If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).</p>
<p>To fix that we need to implement a <code>dragLeaveEvent</code> which hides the indicator.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
</code></pre>
</div>
<p>With those changes, the drag-drop behavior should be working as intended.
The complete code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
def dropEvent(self, e):
widget = e.source()
# Use drop target location for destination, then remove it.
self._drag_target_indicator.hide()
index = self.blayout.indexOf(self._drag_target_indicator)
if index is not None:
self.blayout.insertWidget(index, widget)
self.orderChanged.emit(self.get_item_data())
widget.show()
self.blayout.activate()
e.accept()
def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this example on macOS you may notice that the widget drag preview (the <code>QPixmap</code> created on <code>DragItem</code>) is a bit blurry. On high-resolution screens you need to set the <em>device pixel ratio</em> and scale up the pixmap when
you create it. Below is a modified <code>DragItem</code> class which does this.</p>
<p>Update <code>DragItem</code> to support high resolution screens.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
# Render at x2 pixel ratio to avoid blur on Retina screens.
pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
pixmap.setDevicePixelRatio(2)
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
</code></pre>
</div>
<p>That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p><p>I had an interesting question from a reader of my <a href="https://www.pythonguis.com/pyqt6-book/">PyQt6 book</a>, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.</p>
<blockquote>
<p>I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of <code>QPushButton</code>, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.</p>
</blockquote>
<p>First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#drag-drop-widgets">Drag & Drop Widgets</a></li>
<li><a href="#visual-drag-drop">Visual Drag & Drop</a></li>
<li><a href="#generic-drag-drop-container">Generic Drag & Drop Container</a></li>
<li><a href="#adding-a-visual-drop-target">Adding a Visual Drop Target</a></li>
</ul>
</div>
<h2 id="drag-drop-widgets">Drag & Drop Widgets</h2>
<p>We'll start with a simple application which creates a window using <code>QWidget</code> and places a series of <code>QPushButton</code> widgets into it.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can substitute <code>QPushButton</code> for any other widget you like, e.g. <code>QLabel</code>. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = QPushButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this you should see something like this.</p>
<p><img alt="Widgets in a layout" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/windows-in-layout.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/windows-in-layout.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>The series of <code>QPushButton widgets</code> in a horizontal layout.</em></p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Here we're creating a window, but the <code>Window</code> widget is subclassed from <code>QWidget</code>, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.</p>
<p><code>QPushButton</code> objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>We implement a <code>mouseMoveEvent</code> which accepts the single <code>e</code> parameter of the event. We check to see if the <em>left</em> mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a <code>QDrag</code> object, passing in <code>self</code> to give us access later to the widget that was dragged. We also <em>must</em> pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.</p>
<p>Finally, we initiate a drag by calling <code>drag.exec_(Qt.MoveAction)</code>. As with dialogs <code>exec_()</code> starts a new event loop, blocking the main loop until the drag is complete. The parameter <code>Qt.MoveAction</code> tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.</p>
<p>You can update the main window code to use our new <code>DragButton</code> class as follows.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
</code></pre>
</div>
<p>If you run the code now, you <em>can</em> drag the buttons, but you'll notice the drag is forbidden.</p>
<p><img alt="Drag forbidden" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-forbidden.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-forbidden.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts but is forbidden.</em></p>
<p>What's happening? The mouse movement is being detected by our <code>DragButton</code> object and the drag started, but the main window does not accept drag & drop.</p>
<p>To fix this we need to enable drops on the window and implement <code>dragEnterEvent</code> to actually accept them.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
</code></pre>
</div>
<p>If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling <code>drag.exec_()</code>.</p>
<p><img alt="Drag accepted" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-accepted.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-accepted.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget starts and is accepted, showing a move icon.</em></p>
<p>Releasing the mouse button during a drag drop operation triggers a <code>dropEvent</code> on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our <code>dropEvent</code> method.</p>
<p>The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.</p>
<p>To determine where to place the widget, we iterate over all the widgets in the layout, <em>until</em> we find one who's <code>x</code> position is <em>greater</em> than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.</p>
<p>If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment <code>n</code> one further (in the <code>else:</code> block below).</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x():
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using <code>if pos.x() < w.x() + w.size().width() // 2:</code> -- that is x + half of the width.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
</code></pre>
</div>
<p>The complete working drag-drop code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec(Qt.DropAction.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.blayout = QHBoxLayout()
for l in ["A", "B", "C", "D"]:
btn = DragButton(l)
self.blayout.addWidget(btn)
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if pos.x() < w.x() + w.size().width() // 2:
# We didn't drag past this widget.
# insert to the left of it.
break
else:
# We aren't on the left hand side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
e.accept()
app = QApplication([])
w = Window()
w.show()
app.exec()
</code></pre>
</div>
<h2 id="visual-drag-drop">Visual Drag & Drop</h2>
<p>We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.</p>
<p>Qt's <code>QDrag</code> handler natively provides a mechanism for showing dragged objects which we can use. We can update our <code>DragButton</code> class to pass a <em>pixmap</em> image to <code>QDrag</code> and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a <code>QPixmap</code> of the widget we're dragging.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget
class DragButton(QPushButton):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
</code></pre>
</div>
<p>To create the pixmap we create a <code>QPixmap</code> object passing in the size of the widget this event is fired on with <code>self.size()</code>. This creates an empty <em>pixmap</em> which we can then pass into <code>self.render</code> to <em>render</em> -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the <code>drag</code> object.</p>
<p>If you run the code with this modification you'll see something like the following --</p>
<p><img alt="Drag visual" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-visual.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-visual.png?tr=w-600 600w" loading="lazy" width="421" height="93"/>
<em>Dragging of the widget showing the dragged widget.</em></p>
<h2 id="generic-drag-drop-container">Generic Drag & Drop Container</h2>
<p>We now have a working drag and drop behavior implemented on our window.
We can take this a step further and implement a <em>generic</em> drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget <code>DragWidget</code> which can be added to any window.</p>
<p>You can add <em>items</em> -- instances of <code>DragItem</code> -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal <code>orderChanged</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
break
else:
# We aren't on the left hand/upper side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
self.orderChanged.emit(self.get_item_data())
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p><img alt="Generic drag drop horizontal" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-horizontal.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-horizontal.png?tr=w-600 600w" loading="lazy" width="306" height="139"/>
<em>Generic drag-drop sorting in horizontal orientation.</em></p>
<p>You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal <code>QLabel</code> which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call <code>get_item_data</code> yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort <em>anything</em> not just strings.</p>
<p>In the example above we're passing in the enumerated index as the data, so dragging will output (via the <code>print</code> connected to <code>orderChanged</code>) something like:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]
</code></pre>
</div>
<p>If you remove the <code>item.set_data(n)</code> you'll see the labels emitted on changes.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']
</code></pre>
</div>
<p>We've also implemented <em>orientation</em> onto the <code>DragWidget</code> using the Qt built in flags <code>Qt.Orientation.Vertical</code> or <code>Qt.Orientation.Horizontal</code>. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.</p>
<p><img alt="Generic drag drop vertical" src="https://www.pythonguis.com/static/faq/drag-drop-widgets/drag-generic-vertical.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/faq/drag-drop-widgets/drag-generic-vertical.png?tr=w-600 600w" loading="lazy" width="202" height="216"/>
<em>Generic drag-drop sorting in vertical orientation.</em></p>
<h2 id="adding-a-visual-drop-target">Adding a Visual Drop Target</h2>
<p>If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be
inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using <em>guesswork</em> to get it right.</p>
<p>With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.</p>
<p>In this final section we'll implement this type of drag and drop preview indicator.</p>
<p>The first step is to define our target indicator. This is just another label,
which in our example is empty, with custom styles applied to make it have a
solid "shadow" like background. This makes it obviously different to the
items in the list, so it stands out as something distinct.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.</p>
<p>The drag item is unchanged, but we need to implement some additional behavior on our <code>DragWidget</code> to add the target, control showing and moving it.</p>
<p>First we'll add the drag target indicator to the layout on our <code>DragWidget</code>. This is hidden to begin with, but will be shown during the drag.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
</code></pre>
</div>
<p>Next we modify the <code>DragWidget.dragMoveEvent</code> to show the drag target indicator. We show it by <em>inserting</em> it into the layout and then calling <code>.show</code> -- inserting a widget which is already in a layout will move it.
We also hide the original item which is being dragged.</p>
<p>In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.</p>
<p>Instead, the dragged item is left in place and hidden during move.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
</code></pre>
</div>
<p>The method <code>self._find_drop_location</code> finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.</p>
<p>The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
</code></pre>
</div>
<p>The drop location <code>n</code> is returned for use in the <code>dragMoveEvent</code> to place the drop target indicator.</p>
<p>Next wee need to update the <code>get_item_data</code> handler to ignore the drop target indicator. To do this we check <code>w</code> against <code>self._drag_target_indicator</code> and skip if it is the same. With this change the method will work as expected.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
</code></pre>
</div>
<p>If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).</p>
<p>To fix that we need to implement a <code>dragLeaveEvent</code> which hides the indicator.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"> def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
</code></pre>
</div>
<p>With those changes, the drag-drop behavior should be working as intended.
The complete code is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class DragTargetIndicator(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setContentsMargins(25, 5, 25, 5)
self.setStyleSheet(
"QLabel { background-color: #ccc; border: 1px solid black; }"
)
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
# Add the drag target indicator. This is invisible by default,
# we show it and move it around while the drag is active.
self._drag_target_indicator = DragTargetIndicator()
self.blayout.addWidget(self._drag_target_indicator)
self._drag_target_indicator.hide()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dragLeaveEvent(self, e):
self._drag_target_indicator.hide()
e.accept()
def dragMoveEvent(self, e):
# Find the correct location of the drop target, so we can move it there.
index = self._find_drop_location(e)
if index is not None:
# Inserting moves the item if its alreaady in the layout.
self.blayout.insertWidget(index, self._drag_target_indicator)
# Hide the item being dragged.
e.source().hide()
# Show the target.
self._drag_target_indicator.show()
e.accept()
def dropEvent(self, e):
widget = e.source()
# Use drop target location for destination, then remove it.
self._drag_target_indicator.hide()
index = self.blayout.indexOf(self._drag_target_indicator)
if index is not None:
self.blayout.insertWidget(index, widget)
self.orderChanged.emit(self.get_item_data())
widget.show()
self.blayout.activate()
e.accept()
def _find_drop_location(self, e):
pos = e.position()
spacing = self.blayout.spacing() / 2
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = (
pos.y() >= w.y() - spacing
and pos.y() <= w.y() + w.size().height() + spacing
)
else:
# Drag drop horizontally.
drop_here = (
pos.x() >= w.x() - spacing
and pos.x() <= w.x() + w.size().width() + spacing
)
if drop_here:
# Drop over this target.
break
return n
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if w != self._drag_target_indicator:
# The target indicator has no data.
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for n, l in enumerate(["A", "B", "C", "D"]):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
</code></pre>
</div>
<p>If you run this example on macOS you may notice that the widget drag preview (the <code>QPixmap</code> created on <code>DragItem</code>) is a bit blurry. On high-resolution screens you need to set the <em>device pixel ratio</em> and scale up the pixmap when
you create it. Below is a modified <code>DragItem</code> class which does this.</p>
<p>Update <code>DragItem</code> to support high resolution screens.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
# Render at x2 pixel ratio to avoid blur on Retina screens.
pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
pixmap.setDevicePixelRatio(2)
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
self.show() # Show this widget again, if it's dropped outside.
</code></pre>
</div>
<p>That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/910028356?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>QLineEdit — A Simple Text Input Widget2024-02-05T06:00:00+00:002024-02-05T06:00:00+00:00Leo Welltag:www.pythonguis.com,2024-02-05:/docs/qlineedit-widget/<p>The <code>QLineEdit</code> class is a versatile tool for single-line text input. The widget facilitates text manipulation by supporting insertion, deletion, selection, and cut-copy-paste operations natively. You can use line edits when you need to accept text input from your users in a PyQt/PySide application.</p>
<p>The widget is highly customizable. You can set it up to include placeholder text, input masks, input validators, and more. It also supports many keyboard shortcuts out of the box and is able to process custom ones. This feature opens an opportunity to enhance user input speed and efficiency.</p>
<p>In this article, you will learn the basics of using <code>QLineEdit</code> by going through its most commonly used features and capabilities.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#creating-line-edit-widgets-with-qlineedit">Creating Line Edit Widgets With QLineEdit</a><ul>
<li><a href="#creating-non-editable-line-edits">Creating Non-Editable Line Edits</a></li>
<li><a href="#creating-line-edits-for-passwords">Creating Line Edits for Passwords</a></li>
</ul>
</li>
<li><a href="#manipulating-the-input-in-a-line-edit">Manipulating the Input in a Line Edit</a></li>
<li><a href="#aligning-and-formatting-the-text-in-a-line-edit">Aligning and Formatting the Text in a Line Edit</a></li>
<li><a href="#connecting-signals-and-slots">Connecting Signals and Slots</a></li>
<li><a href="#validating-input-in-line-edits">Validating Input in Line Edits</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="creating-line-edit-widgets-with-qlineedit">Creating Line Edit Widgets With <code>QLineEdit</code></h2>
<p>A line edit allows the user to enter and edit a single line of plain text with a useful collection of editing functionalities, such as insertion, deletion, selection, cut-copy-paste, and drag-and-drop operations.</p>
<p>If you've used <a href="https://www.pythonguis.com/pyqt6/">PyQt</a> or <a href="https://www.pythonguis.com/pyside6/">PySide</a> to create GUI applications in Python, then it'd be likely that you already know about the <a href="https://doc.qt.io/qt-6/qlineedit.html"><code>QLineEdit</code></a> class. This class allows you to create line edits in your graphical interfaces (GUI) quickly.</p>
<p>The <code>QLineEidt</code> class provides two different constructors that you can use to create line edits according to your needs:</p>
<table>
<thead>
<tr>
<th>Constructor</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#QLineEdit"><code>QLineEdit(parent: QWidget = None)</code></a></td>
<td>Constructs a line edit with a parent widget but without text</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#QLineEdit-1"><code>QLineEdit(text: str, parent: QWidget = None)</code></a></td>
<td>Constructs a line edit with default text and a parent widget</td>
</tr>
</tbody>
</table>
<p>The parent widget would be the window or dialog where you need to place the line edit. The text can be a default text that will appear in the line edit when you run the application.</p>
<p>To illustrate how to use the above constructors, we can code a demo example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("QLineEdit Constructors")
self.resize(300, 100)
# Line edit with a parent widget
top_line_edit = QLineEdit(parent=self)
# Line edit with a parent widget and a default text
bottom_line_edit = QLineEdit(
"Hello! This is a line edit.", parent=self
)
layout = QVBoxLayout()
layout.addWidget(top_line_edit)
layout.addWidget(bottom_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we first do the required imports and then define the <code>Window</code> class inheriting from <code>QWidget</code>. Inside <code>Window</code>, we create two <code>QLineEdit</code> widgets.</p>
<p>To create the first line edit, we use the first constructor of <code>QLineEdit</code>, passing only a <code>parent</code> widget. For the second line editor, we use the second constructor, which requires the parent widget and a default text. Note that the text is a regular Python string.</p>
<p>Save the code to a file called <code>constructors.py</code> file and run it from your command line. You'll get a window that looks something like this:</p>
<p><img alt="QLineEdit constructors example" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-constructors.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>Standard window showing our two line edits.</em></p>
<p>The first line edit has no text. In most cases, this is how you would create your line edits because they're designed for accepting input. If you'd like to just display some text, then you can use a <code>QLabel</code> widget instead. The second line edit displays the text that you passed to the constructor.</p>
<p>Both line edits are ready for accepting input text. Note that you can use all the following keyboard shortcuts to optimize your text input process:</p>
<table>
<thead>
<tr>
<th align="left">Keys</th>
<th align="left">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Left Arrow</td>
<td align="left">Moves the cursor one character to the left</td>
</tr>
<tr>
<td align="left">Shift+Left Arrow</td>
<td align="left">Moves and selects text one character to the left</td>
</tr>
<tr>
<td align="left">Right Arrow</td>
<td align="left">Moves the cursor one character to the right</td>
</tr>
<tr>
<td align="left">Shift+Right Arrow</td>
<td align="left">Moves and selects text one character to the right</td>
</tr>
<tr>
<td align="left">Home</td>
<td align="left">Moves the cursor to the beginning of the line</td>
</tr>
<tr>
<td align="left">End</td>
<td align="left">Moves the cursor to the end of the line</td>
</tr>
<tr>
<td align="left">Backspace</td>
<td align="left">Deletes the character to the left of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+Backspace</td>
<td align="left">Deletes the word to the left of the cursor</td>
</tr>
<tr>
<td align="left">Delete</td>
<td align="left">Deletes the character to the right of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+Delete</td>
<td align="left">Deletes the word to the right of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+A</td>
<td align="left">Select all</td>
</tr>
<tr>
<td align="left">Ctrl+C</td>
<td align="left">Copies the selected text to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+Insert</td>
<td align="left">Copies the selected text to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+K</td>
<td align="left">Deletes to the end of the line</td>
</tr>
<tr>
<td align="left">Ctrl+V</td>
<td align="left">Pastes the clipboard text into the line edit</td>
</tr>
<tr>
<td align="left">Shift+Insert</td>
<td align="left">Pastes the clipboard text into the line edit</td>
</tr>
<tr>
<td align="left">Ctrl+X</td>
<td align="left">Deletes the selected text and copies it to the clipboard</td>
</tr>
<tr>
<td align="left">Shift+Delete</td>
<td align="left">Deletes the selected text and copies it to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+Z</td>
<td align="left">Undoes the last operation</td>
</tr>
<tr>
<td align="left">Ctrl+Y</td>
<td align="left">Redoes the last undone operation</td>
</tr>
</tbody>
</table>
<p>That's a lot of shortcuts! This table is just a sample of all the features that the <code>QLineEdit</code> class provides out of the box.</p>
<p>In addition to these keyboard shortcuts, the <code>QLineEdit</code> class provides a context menu that you can trigger by clicking on a line edit using the right button of your mouse:</p>
<p><img alt="QLineEdit context menu" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-context-menu.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-600 600w" loading="lazy" width="662" height="592"/>
<em>QLineEdit with a context menu visible.</em></p>
<p>The built-in context menu provides basic edition options, such as cut, copy, paste, and delete. It also has options for undoing and redoing edits, and for selecting all the content of a given line edit.</p>
<h3 id="creating-non-editable-line-edits">Creating Non-Editable Line Edits</h3>
<p>Sometimes, we need to make a line edit non-editable. By default, all line edits are editable because their main use case is to provide text input for the user. However, in some situations, a non-editable line edit can be useful.</p>
<p>For example, say that you're creating a GUI application that generates some recovery keys for the users. The user must be able to copy the key to a safe place so that they can use it to recover access if they forget their password. In this situation, creating a non-editable line edit can provide a suitable solution.</p>
<p>To make a line edit read-only, we can use the <a href="https://doc.qt.io/qt-6/qlineedit.html#readOnly-prop"><code>readOnly</code></a> property and its setter method <code>setReadOnly()</code> as in the following example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import secrets
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QLineEdit,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Non-editable QLineEdit")
self.resize(300, 100)
non_editable_line_edit = QLineEdit(parent=self)
non_editable_line_edit.setReadOnly(True)
non_editable_line_edit.setText(secrets.token_hex(16))
layout = QVBoxLayout()
layout.addWidget(QLabel(parent=self, text="Your secret key:"))
layout.addWidget(non_editable_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we create a non-editable line edit by using the <code>setReadOnly()</code> method. When we set the <code>readOnly</code> property to <code>True</code>, our line edit won't accept editions. It'll only allow us to select and copy its content.</p>
<p>Go ahead and run the application from your command line to explore how this line edit works. You'll get a window like the following:</p>
<p><img alt="Non-editable line edit" src="https://www.pythonguis.com/static/docs/qlineedit/non-editable-lineedit.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-600 600w" loading="lazy" width="662" height="424"/>
<em>A read-only line edit with editing disabled.</em></p>
<p>If you play a bit with this line edit, you'll soon discover that you can't change its text. You'll also note that the options in the context menu are now limited to <em>Copy</em> and <em>Select All</em>.</p>
<h3 id="creating-line-edits-for-passwords">Creating Line Edits for Passwords</h3>
<p>Another cool feature of the <code>QLineEdit</code> class is that it allows you to create text input for passwords. This can be pretty convenient for those applications that manage several users, and each user needs to have access credentials.</p>
<p>You can create line edits for passwords by using the <a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/api/qtwidgets/qlineedit.html#echoMode"><code>echoMode()</code></a> method. This method takes one of the following constants as an argument:</p>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>QLineEdit.EchoMode.Normal</code></td>
<td><code>0</code></td>
<td>Display characters as you enter them.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.NoEcho</code></td>
<td><code>1</code></td>
<td>Display no characters when you enter them.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.Password</code></td>
<td><code>2</code></td>
<td>Display platform-dependent password mask characters instead of the characters you enter.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.PasswordEchoOnEdit</code></td>
<td><code>3</code></td>
<td>Display characters as you enter them while editing. Display characters as the <code>Password</code> mode does when reading.</td>
</tr>
</tbody>
</table>
<p>The <code>Normal</code> mode is the default. The <code>NoEcho</code> mode may be appropriate for critical passwords where even the length of the password should be kept secret.
The <code>Password</code> mode is appropriate for most password use cases, however <code>PasswordEchoOnEdit</code> can be used instead if you need to give users some confirmation of what they are typing.</p>
<p>Here's a sample app that shows a user and password form:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QFormLayout,
QLineEdit,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Password QLineEdit")
self.resize(300, 100)
username_line_edit = QLineEdit(parent=self)
password_line_edit = QLineEdit(parent=self)
password_line_edit.setEchoMode(QLineEdit.EchoMode.Password)
layout = QFormLayout()
layout.addRow("Username:", username_line_edit)
layout.addRow("Password:", password_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, you call <code>setEchoMode()</code> on the <code>password_line_edit</code> widget using the <code>Password</code> mode as an argument. When you run this code from your command line, you get the following window on your screen:</p>
<p><img alt="Password line edit" src="https://www.pythonguis.com/static/docs/qlineedit/password-qlineedit.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>Window with a username and password line edit.</em></p>
<p>The <code>username_line_edit</code> line edit is in <code>Normal</code> mode, so we can see the characters as we type them in. In contrast, the Password line edit is in <code>Password</code> mode. In this case, when we enter a character, the line edit shows the platform's character for passwords.</p>
<h2 id="manipulating-the-input-in-a-line-edit">Manipulating the Input in a Line Edit</h2>
<p>You can change the text of a line edit using the <a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>setText()</code></a> or <a href="https://doc.qt.io/qt-6/qlineedit.html#insert"><code>insert()</code></a> methods. You can retrieve the text with the <a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>text()</code></a> method. However, these are not the only operations that you can perform with the text of a line edit.</p>
<p>The following table shows a summary of some of the most commonly used methods for text manipulation in line edits:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>setText(text)</code></a></td>
<td>Sets the text of a line edit to <code>text</code>, clears the selection, clears the undo/redo history, moves the cursor to the end of the line, and resets the <a href="https://doc.qt.io/qt-6/qlineedit.html#modified-prop">modified</a> property to false.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#insert"><code>insert(text)</code></a></td>
<td>Deletes any selected text, inserts <code>text</code>, and validates the result. If it is valid, it sets it as the new contents of the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#clear"><code>clear()</code></a></td>
<td>Clears the contents of the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#copy"><code>copy()</code></a></td>
<td>Copies the selected text to the clipboard.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#cut"><code>cut()</code></a></td>
<td>Copies the selected text to the clipboard and deletes it from the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#paste"><code>paste()</code></a></td>
<td>Inserts the clipboard's text at the cursor position, deleting any selected text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#redo"><code>redo()</code></a></td>
<td>Redoes the last operation if redo is <a href="https://doc.qt.io/qt-6/qlineedit.html#redoAvailable-prop">available</a>. Redo becomes available once the user has performed one or more undo operations on text in the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#undo"><code>undo()</code></a></td>
<td>Undoes the last operation if undo is <a href="https://doc.qt.io/qt-6/qlineedit.html#undoAvailable-prop">available</a>. Undo becomes available once the user has modified the text in the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#selectAll"><code>selectAll()</code></a></td>
<td>Selects all the text and moves the cursor to the end.</td>
</tr>
</tbody>
</table>
<p>You can use any of these methods to manipulate the text of a line edit from your code. Consider the following example where you have two line edits and two buttons that take advantage of some of the above methods to copy some text from one line edit to the other:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QGridLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Copy and Paste")
self.resize(300, 100)
self.source_line_edit = QLineEdit(parent=self)
self.source_line_edit.setText("Hello, World!")
self.dest_line_edit = QLineEdit(parent=self)
copy_button = QPushButton(parent=self, text="Copy")
paste_button = QPushButton(parent=self, text="Paste")
copy_button.clicked.connect(self.copy)
paste_button.clicked.connect(self.paste)
layout = QGridLayout()
layout.addWidget(self.source_line_edit, 0, 0)
layout.addWidget(copy_button, 0, 1)
layout.addWidget(self.dest_line_edit, 1, 0)
layout.addWidget(paste_button, 1, 1)
self.setLayout(layout)
def copy(self):
self.source_line_edit.selectAll()
self.source_line_edit.copy()
def paste(self):
self.dest_line_edit.clear()
self.dest_line_edit.paste()
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we create two line edits. The first one will hold some sample text. The second line edit will receive the text. Then, we create two buttons and connect their <code>clicked</code> signals to the <code>copy()</code> and <code>paste()</code> slots.</p>
<p>Inside the <code>copy()</code> method we first select all the text from the source line edit. Then we use the <code>copy()</code> method to copy the selected text to the clipboard. In <code>paste()</code>, we call <code>clear()</code> on the destination line edit to remove any previous text. Then, we use the <code>paste()</code> method to copy the clipboard's content.</p>
<p>Go ahead and run the application. You'll get the following window on your screen:</p>
<p><img alt="QLineEdit Copy and Paste" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-manipulation.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with Copy & Paste buttons attached to handlers.</em></p>
<p>Once you've run the app, then you can click the <em>Copy</em> button to copy the text in the first line edit. Next, you can click the <em>Paste</em> button to paste the copied text to the second line edit. Go ahead and give it a try!</p>
<h2 id="aligning-and-formatting-the-text-in-a-line-edit">Aligning and Formatting the Text in a Line Edit</h2>
<p>You can also align and format the text in a line edit. For example, for text alignment, you can use the <a href="https://doc.qt.io/qt-6/qlineedit.html#alignment-prop"><code>setAlignment()</code></a> method with one or more alignment flags. Some of the most useful flags that you can find in the <a href="https://doc.qt.io/qt-6/qt.html#AlignmentFlag-enum"><code>Qt.AlignmentFlag</code></a> namespace includes the following:</p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AlignLeft</code></td>
<td>Aligns with the left edge.</td>
</tr>
<tr>
<td><code>AlignRight</code></td>
<td>Aligns with the right edge.</td>
</tr>
<tr>
<td><code>AlignHCenter</code></td>
<td>Centers horizontally in the available space.</td>
</tr>
<tr>
<td><code>AlignJustify</code></td>
<td>Justifies the text in the available space.</td>
</tr>
<tr>
<td><code>AlignTop</code></td>
<td>Aligns with the top.</td>
</tr>
<tr>
<td><code>AlignBottom</code></td>
<td>Aligns with the bottom.</td>
</tr>
<tr>
<td><code>AlignVCenter</code></td>
<td>Centers vertically in the available space.</td>
</tr>
<tr>
<td><code>AlignCenter</code></td>
<td>Centers in both dimensions.</td>
</tr>
</tbody>
</table>
<p>If you want to apply multiple alignment flags to a given line edit, you don't have to call <code>setAlignment()</code> multiple times. You can just use the bitwise OR operator (<code>|</code>) to combine them. Consider the following example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLineEdit
class Window(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Aligning Text")
self.resize(300, 100)
self.setText("Hello, World!")
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we use a <code>QLineEdit</code> as the only component of our app's GUI. Using the <code>setAlignment()</code> method, we center the <code>"Hello, World!"</code> message in both directions, horizontally and vertically. To do this, we use the <code>AlignCenter</code> flag. Here's what the app looks like:</p>
<p><img alt="QLineEdit text alignment" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-alignment.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with text alignment set.</em></p>
<p>Go ahead and play with other flags to see their effect on the text alignment. Use the <code>|</code> bitwise operator to combine several alignment flags.</p>
<p>Line edits also have a <code>textMargins</code> property that you can tweak to customize the text alignment using specific values. To set margin values for your text, you can use the <code>setTextMargins()</code> method, which has the following signatures:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#setTextMargins"><code>setTextMargins(left, top, right, bottom)</code></a></td>
<td>Sets the margins around the text to have the sizes <code>left</code>, <code>top</code>, <code>right</code>, and <code>bottom</code>.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#setTextMargins-1"><code>setTextMargins(margins)</code></a></td>
<td>Sets the <code>margins</code> around the text using a <a href="https://doc.qt.io/qt-6/qmargins.html"><code>QMargins</code></a> object.</td>
</tr>
</tbody>
</table>
<p>The first signature allows you to provide four integer values as the left, top, right, and bottom margins for the text. The second signature accepts a <code>QMargins</code> object as an argument. To build this object, you can use four integer values with the same meaning as the <code>left</code>, <code>top</code>, <code>right</code>, and <code>bottom</code> arguments in the first signature.</p>
<p>Here's an example of how to set custom margins for the text in a line edit:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QLineEdit
class Window(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Aligning Text")
self.resize(300, 100)
self.setText("Hello, World!")
self.setTextMargins(30, 30, 0, 0)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, you set the left and top margins to custom values. Here's how this app looks when you run it:</p>
<p><img alt="QLineEdit text margins" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-margins.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with text margins added.</em></p>
<p>Using the <code>setTextMargins()</code> method, we can place the text in the desired place in a line edit, which may be a requirement in some situations.</p>
<h2 id="connecting-signals-and-slots">Connecting Signals and Slots</h2>
<p>When you're creating a GUI application and you need to use line edits, you may need to perform actions when the user enters or modifies the content of the line edit. To do this, you need to connect some of the signals of the line edit to specific slots or functions.</p>
<p>Depending on specific user events, the <code>QLineEdit</code> class can emit several different signals. Here's is a summary of these signals and their corresponding meaning:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Emitted</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#textChanged"><code>textChanged(text)</code></a></td>
<td>Whenever the user changes the text either manually or programmatically. The <code>text</code> argument is the new text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#textEdited"><code>textEdited(text)</code></a></td>
<td>Whenever the user edits the text manually. The <code>text</code> argument is the new text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#editingFinished"><code>editingFinished</code></a></td>
<td>When the user presses the <em>Return</em> or <em>Enter</em> key, or when the line edit loses focus, and its contents have changed since the last time this signal was emitted.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#inputRejected"><code>inputRejected</code></a></td>
<td>When the user presses a key that is an unacceptable input.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#returnPressed"><code>returnPressed</code></a></td>
<td>When the user presses the <em>Return</em> or <em>Enter</em> key.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#selectionChanged"><code>selectionChanged</code></a></td>
<td>When the selection changes.</td>
</tr>
</tbody>
</table>
<p>We can connect either of these signals with any slot. A slot is a method or function that performs a concrete action in your application. We can connect a signal and a slot so that the slot gets called when the signal gets emitted. Here's the required syntax to do this:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">line_edit.<signal>.connect(<method>)
</code></pre>
</div>
<p>In this construct, <code>line_edit</code> is the <code>QLineEdit</code> object that we need to connect with a given slot. The <code><signal></code> placeholder can be any of the abovementioned signals. Finally, <code><method></code> represents the target slot or method.</p>
<p>Let's write an example that puts this syntax into action. For this example, we'll connect the <code>textEdited</code> signal with a method that updates the text of a <code>QLabel</code> to match the text of our line edit:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QLabel,
QLineEdit,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Signal and Slot")
self.resize(300, 100)
self.line_edit = QLineEdit(parent=self)
self.label = QLabel(parent=self)
self.line_edit.textEdited.connect(self.update_label)
layout = QVBoxLayout()
layout.addWidget(self.line_edit)
layout.addWidget(self.label)
self.setLayout(layout)
def update_label(self):
self.label.setText(self.line_edit.text())
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we connect the line edit's <code>textEdited</code> signal with the <code>update_label()</code> method, which sets the label's text to match the text we enter in our line edit. Go ahead and run the app. Then, enter some text in the line edit and see what happens with the label at the bottom of the app's window.</p>
<h2 id="validating-input-in-line-edits">Validating Input in Line Edits</h2>
<p>We can provide input validators to our line edits using the <a href="https://doc.qt.io/qt-6/qlineedit.html#setValidator"><code>setValidator()</code></a> method. This method takes a <code>QValidator</code> object as an argument. PyQt has a few built-in validators that you can use directly on a line edit:</p>
<ul>
<li><a href="https://doc.qt.io/qt-6/qintvalidator.html">QIntValidator</a> for integer validation</li>
<li><a href="https://doc.qt.io/qt-6/qdoublevalidator.html">QDoubleValidator</a> for floating-point validation</li>
<li><a href="https://doc.qt.io/qt-6/qregularexpressionvalidator.html">QRegularExpressionValidator</a> for validation based on regular expressions</li>
</ul>
<p>Validator objects process the input to check whether it's valid. In general, validator object has three possible states:</p>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>QValidator.State.Invalid</code></td>
<td><code>0</code></td>
<td>The input is invalid.</td>
</tr>
<tr>
<td><code>QValidator.State.Intermediate</code></td>
<td><code>1</code></td>
<td>The input is a valid intermediate value.</td>
</tr>
<tr>
<td><code>QValidator.State.Acceptable</code></td>
<td><code>2</code></td>
<td>The input is acceptable as a final result.</td>
</tr>
</tbody>
</table>
<p>When you set a validator to a given line edit, the user may change the content to any <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Intermediate</code></a> value during editing. However, they can't edit the text to a value that is <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Invalid</code></a>. The line edit will emit the <code>returnPressed</code> and <code>editingFinished</code> signals only when the validator validates input as <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Acceptable</code></a>.</p>
<p>Here's an example where we use a <code>QIntValidator</code> and a <code>QRegularExpressionValidator</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QIntValidator, QRegularExpressionValidator
from PyQt6.QtWidgets import (
QApplication,
QFormLayout,
QLabel,
QLineEdit,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Input Validators")
self.resize(300, 100)
self.int_line_edit = QLineEdit(parent=self)
self.int_line_edit.setValidator(QIntValidator())
self.uppercase_line_edit = QLineEdit(parent=self)
input_validator = QRegularExpressionValidator(
QRegularExpression("[A-Z]+"), self.uppercase_line_edit
)
self.uppercase_line_edit.setValidator(input_validator)
self.uppercase_line_edit.returnPressed.connect(self.update_label)
self.signal_label = QLabel(parent=self)
layout = QFormLayout()
layout.addRow("Integers:", self.int_line_edit)
layout.addRow("Uppercase letters:", self.uppercase_line_edit)
layout.addRow("Signal:", self.signal_label)
self.setLayout(layout)
def update_label(self):
self.signal_label.setText("Return pressed!")
if __name__ == "__main__":
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we have two line edits. In the first line edit, we've used a <code>QIntValidator</code> object. This way, the line edit will only accept valid integer numbers. If you try to type in something different, then the line edit won't accept your input.</p>
<p>In the second line edit, we've used a <code>QRegularExpressionValidator</code>. In this specific case, the regular expression accepts one or more uppercase letters. Again if you try to enter something else, then the line edit won't accept the input.</p>
<p>The <code>QLabel</code> will show the text <code>Return pressed!</code> if the input in the second line edit is valid and you press the Enter key. Here's what the app looks like:</p>
<p><img alt="QLineEdit with input validator" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-input-validators.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-600 600w" loading="lazy" width="600" height="292"/>
<em>QLineEdit with input validator.</em></p>
<p>Validators provide a quick way to restrict the user input and set our own validation rules. This way, we can ensure valid user input in our applications.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Line edits are pretty useful widgets in any GUI application. They allow text input through a single-line editor that has many cool features.</p>
<p>In this tutorial, you've learned how to create, use, and customize your line edits while building a GUI application with PyQt/PySide.</p><p>The <code>QLineEdit</code> class is a versatile tool for single-line text input. The widget facilitates text manipulation by supporting insertion, deletion, selection, and cut-copy-paste operations natively. You can use line edits when you need to accept text input from your users in a PyQt/PySide application.</p>
<p>The widget is highly customizable. You can set it up to include placeholder text, input masks, input validators, and more. It also supports many keyboard shortcuts out of the box and is able to process custom ones. This feature opens an opportunity to enhance user input speed and efficiency.</p>
<p>In this article, you will learn the basics of using <code>QLineEdit</code> by going through its most commonly used features and capabilities.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#creating-line-edit-widgets-with-qlineedit">Creating Line Edit Widgets With QLineEdit</a><ul>
<li><a href="#creating-non-editable-line-edits">Creating Non-Editable Line Edits</a></li>
<li><a href="#creating-line-edits-for-passwords">Creating Line Edits for Passwords</a></li>
</ul>
</li>
<li><a href="#manipulating-the-input-in-a-line-edit">Manipulating the Input in a Line Edit</a></li>
<li><a href="#aligning-and-formatting-the-text-in-a-line-edit">Aligning and Formatting the Text in a Line Edit</a></li>
<li><a href="#connecting-signals-and-slots">Connecting Signals and Slots</a></li>
<li><a href="#validating-input-in-line-edits">Validating Input in Line Edits</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="creating-line-edit-widgets-with-qlineedit">Creating Line Edit Widgets With <code>QLineEdit</code></h2>
<p>A line edit allows the user to enter and edit a single line of plain text with a useful collection of editing functionalities, such as insertion, deletion, selection, cut-copy-paste, and drag-and-drop operations.</p>
<p>If you've used <a href="https://www.pythonguis.com/pyqt6/">PyQt</a> or <a href="https://www.pythonguis.com/pyside6/">PySide</a> to create GUI applications in Python, then it'd be likely that you already know about the <a href="https://doc.qt.io/qt-6/qlineedit.html"><code>QLineEdit</code></a> class. This class allows you to create line edits in your graphical interfaces (GUI) quickly.</p>
<p>The <code>QLineEidt</code> class provides two different constructors that you can use to create line edits according to your needs:</p>
<table>
<thead>
<tr>
<th>Constructor</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#QLineEdit"><code>QLineEdit(parent: QWidget = None)</code></a></td>
<td>Constructs a line edit with a parent widget but without text</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#QLineEdit-1"><code>QLineEdit(text: str, parent: QWidget = None)</code></a></td>
<td>Constructs a line edit with default text and a parent widget</td>
</tr>
</tbody>
</table>
<p>The parent widget would be the window or dialog where you need to place the line edit. The text can be a default text that will appear in the line edit when you run the application.</p>
<p>To illustrate how to use the above constructors, we can code a demo example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("QLineEdit Constructors")
self.resize(300, 100)
# Line edit with a parent widget
top_line_edit = QLineEdit(parent=self)
# Line edit with a parent widget and a default text
bottom_line_edit = QLineEdit(
"Hello! This is a line edit.", parent=self
)
layout = QVBoxLayout()
layout.addWidget(top_line_edit)
layout.addWidget(bottom_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we first do the required imports and then define the <code>Window</code> class inheriting from <code>QWidget</code>. Inside <code>Window</code>, we create two <code>QLineEdit</code> widgets.</p>
<p>To create the first line edit, we use the first constructor of <code>QLineEdit</code>, passing only a <code>parent</code> widget. For the second line editor, we use the second constructor, which requires the parent widget and a default text. Note that the text is a regular Python string.</p>
<p>Save the code to a file called <code>constructors.py</code> file and run it from your command line. You'll get a window that looks something like this:</p>
<p><img alt="QLineEdit constructors example" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-constructors.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-constructors.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>Standard window showing our two line edits.</em></p>
<p>The first line edit has no text. In most cases, this is how you would create your line edits because they're designed for accepting input. If you'd like to just display some text, then you can use a <code>QLabel</code> widget instead. The second line edit displays the text that you passed to the constructor.</p>
<p>Both line edits are ready for accepting input text. Note that you can use all the following keyboard shortcuts to optimize your text input process:</p>
<table>
<thead>
<tr>
<th align="left">Keys</th>
<th align="left">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Left Arrow</td>
<td align="left">Moves the cursor one character to the left</td>
</tr>
<tr>
<td align="left">Shift+Left Arrow</td>
<td align="left">Moves and selects text one character to the left</td>
</tr>
<tr>
<td align="left">Right Arrow</td>
<td align="left">Moves the cursor one character to the right</td>
</tr>
<tr>
<td align="left">Shift+Right Arrow</td>
<td align="left">Moves and selects text one character to the right</td>
</tr>
<tr>
<td align="left">Home</td>
<td align="left">Moves the cursor to the beginning of the line</td>
</tr>
<tr>
<td align="left">End</td>
<td align="left">Moves the cursor to the end of the line</td>
</tr>
<tr>
<td align="left">Backspace</td>
<td align="left">Deletes the character to the left of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+Backspace</td>
<td align="left">Deletes the word to the left of the cursor</td>
</tr>
<tr>
<td align="left">Delete</td>
<td align="left">Deletes the character to the right of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+Delete</td>
<td align="left">Deletes the word to the right of the cursor</td>
</tr>
<tr>
<td align="left">Ctrl+A</td>
<td align="left">Select all</td>
</tr>
<tr>
<td align="left">Ctrl+C</td>
<td align="left">Copies the selected text to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+Insert</td>
<td align="left">Copies the selected text to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+K</td>
<td align="left">Deletes to the end of the line</td>
</tr>
<tr>
<td align="left">Ctrl+V</td>
<td align="left">Pastes the clipboard text into the line edit</td>
</tr>
<tr>
<td align="left">Shift+Insert</td>
<td align="left">Pastes the clipboard text into the line edit</td>
</tr>
<tr>
<td align="left">Ctrl+X</td>
<td align="left">Deletes the selected text and copies it to the clipboard</td>
</tr>
<tr>
<td align="left">Shift+Delete</td>
<td align="left">Deletes the selected text and copies it to the clipboard</td>
</tr>
<tr>
<td align="left">Ctrl+Z</td>
<td align="left">Undoes the last operation</td>
</tr>
<tr>
<td align="left">Ctrl+Y</td>
<td align="left">Redoes the last undone operation</td>
</tr>
</tbody>
</table>
<p>That's a lot of shortcuts! This table is just a sample of all the features that the <code>QLineEdit</code> class provides out of the box.</p>
<p>In addition to these keyboard shortcuts, the <code>QLineEdit</code> class provides a context menu that you can trigger by clicking on a line edit using the right button of your mouse:</p>
<p><img alt="QLineEdit context menu" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-context-menu.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-context-menu.png?tr=w-600 600w" loading="lazy" width="662" height="592"/>
<em>QLineEdit with a context menu visible.</em></p>
<p>The built-in context menu provides basic edition options, such as cut, copy, paste, and delete. It also has options for undoing and redoing edits, and for selecting all the content of a given line edit.</p>
<h3 id="creating-non-editable-line-edits">Creating Non-Editable Line Edits</h3>
<p>Sometimes, we need to make a line edit non-editable. By default, all line edits are editable because their main use case is to provide text input for the user. However, in some situations, a non-editable line edit can be useful.</p>
<p>For example, say that you're creating a GUI application that generates some recovery keys for the users. The user must be able to copy the key to a safe place so that they can use it to recover access if they forget their password. In this situation, creating a non-editable line edit can provide a suitable solution.</p>
<p>To make a line edit read-only, we can use the <a href="https://doc.qt.io/qt-6/qlineedit.html#readOnly-prop"><code>readOnly</code></a> property and its setter method <code>setReadOnly()</code> as in the following example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import secrets
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QLineEdit,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Non-editable QLineEdit")
self.resize(300, 100)
non_editable_line_edit = QLineEdit(parent=self)
non_editable_line_edit.setReadOnly(True)
non_editable_line_edit.setText(secrets.token_hex(16))
layout = QVBoxLayout()
layout.addWidget(QLabel(parent=self, text="Your secret key:"))
layout.addWidget(non_editable_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we create a non-editable line edit by using the <code>setReadOnly()</code> method. When we set the <code>readOnly</code> property to <code>True</code>, our line edit won't accept editions. It'll only allow us to select and copy its content.</p>
<p>Go ahead and run the application from your command line to explore how this line edit works. You'll get a window like the following:</p>
<p><img alt="Non-editable line edit" src="https://www.pythonguis.com/static/docs/qlineedit/non-editable-lineedit.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/non-editable-lineedit.png?tr=w-600 600w" loading="lazy" width="662" height="424"/>
<em>A read-only line edit with editing disabled.</em></p>
<p>If you play a bit with this line edit, you'll soon discover that you can't change its text. You'll also note that the options in the context menu are now limited to <em>Copy</em> and <em>Select All</em>.</p>
<h3 id="creating-line-edits-for-passwords">Creating Line Edits for Passwords</h3>
<p>Another cool feature of the <code>QLineEdit</code> class is that it allows you to create text input for passwords. This can be pretty convenient for those applications that manage several users, and each user needs to have access credentials.</p>
<p>You can create line edits for passwords by using the <a href="https://www.riverbankcomputing.com/static/Docs/PyQt6/api/qtwidgets/qlineedit.html#echoMode"><code>echoMode()</code></a> method. This method takes one of the following constants as an argument:</p>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>QLineEdit.EchoMode.Normal</code></td>
<td><code>0</code></td>
<td>Display characters as you enter them.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.NoEcho</code></td>
<td><code>1</code></td>
<td>Display no characters when you enter them.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.Password</code></td>
<td><code>2</code></td>
<td>Display platform-dependent password mask characters instead of the characters you enter.</td>
</tr>
<tr>
<td><code>QLineEdit.EchoMode.PasswordEchoOnEdit</code></td>
<td><code>3</code></td>
<td>Display characters as you enter them while editing. Display characters as the <code>Password</code> mode does when reading.</td>
</tr>
</tbody>
</table>
<p>The <code>Normal</code> mode is the default. The <code>NoEcho</code> mode may be appropriate for critical passwords where even the length of the password should be kept secret.
The <code>Password</code> mode is appropriate for most password use cases, however <code>PasswordEchoOnEdit</code> can be used instead if you need to give users some confirmation of what they are typing.</p>
<p>Here's a sample app that shows a user and password form:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QFormLayout,
QLineEdit,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Password QLineEdit")
self.resize(300, 100)
username_line_edit = QLineEdit(parent=self)
password_line_edit = QLineEdit(parent=self)
password_line_edit.setEchoMode(QLineEdit.EchoMode.Password)
layout = QFormLayout()
layout.addRow("Username:", username_line_edit)
layout.addRow("Password:", password_line_edit)
self.setLayout(layout)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, you call <code>setEchoMode()</code> on the <code>password_line_edit</code> widget using the <code>Password</code> mode as an argument. When you run this code from your command line, you get the following window on your screen:</p>
<p><img alt="Password line edit" src="https://www.pythonguis.com/static/docs/qlineedit/password-qlineedit.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/password-qlineedit.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>Window with a username and password line edit.</em></p>
<p>The <code>username_line_edit</code> line edit is in <code>Normal</code> mode, so we can see the characters as we type them in. In contrast, the Password line edit is in <code>Password</code> mode. In this case, when we enter a character, the line edit shows the platform's character for passwords.</p>
<h2 id="manipulating-the-input-in-a-line-edit">Manipulating the Input in a Line Edit</h2>
<p>You can change the text of a line edit using the <a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>setText()</code></a> or <a href="https://doc.qt.io/qt-6/qlineedit.html#insert"><code>insert()</code></a> methods. You can retrieve the text with the <a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>text()</code></a> method. However, these are not the only operations that you can perform with the text of a line edit.</p>
<p>The following table shows a summary of some of the most commonly used methods for text manipulation in line edits:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#text-prop"><code>setText(text)</code></a></td>
<td>Sets the text of a line edit to <code>text</code>, clears the selection, clears the undo/redo history, moves the cursor to the end of the line, and resets the <a href="https://doc.qt.io/qt-6/qlineedit.html#modified-prop">modified</a> property to false.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#insert"><code>insert(text)</code></a></td>
<td>Deletes any selected text, inserts <code>text</code>, and validates the result. If it is valid, it sets it as the new contents of the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#clear"><code>clear()</code></a></td>
<td>Clears the contents of the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#copy"><code>copy()</code></a></td>
<td>Copies the selected text to the clipboard.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#cut"><code>cut()</code></a></td>
<td>Copies the selected text to the clipboard and deletes it from the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#paste"><code>paste()</code></a></td>
<td>Inserts the clipboard's text at the cursor position, deleting any selected text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#redo"><code>redo()</code></a></td>
<td>Redoes the last operation if redo is <a href="https://doc.qt.io/qt-6/qlineedit.html#redoAvailable-prop">available</a>. Redo becomes available once the user has performed one or more undo operations on text in the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#undo"><code>undo()</code></a></td>
<td>Undoes the last operation if undo is <a href="https://doc.qt.io/qt-6/qlineedit.html#undoAvailable-prop">available</a>. Undo becomes available once the user has modified the text in the line edit.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#selectAll"><code>selectAll()</code></a></td>
<td>Selects all the text and moves the cursor to the end.</td>
</tr>
</tbody>
</table>
<p>You can use any of these methods to manipulate the text of a line edit from your code. Consider the following example where you have two line edits and two buttons that take advantage of some of the above methods to copy some text from one line edit to the other:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QGridLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Copy and Paste")
self.resize(300, 100)
self.source_line_edit = QLineEdit(parent=self)
self.source_line_edit.setText("Hello, World!")
self.dest_line_edit = QLineEdit(parent=self)
copy_button = QPushButton(parent=self, text="Copy")
paste_button = QPushButton(parent=self, text="Paste")
copy_button.clicked.connect(self.copy)
paste_button.clicked.connect(self.paste)
layout = QGridLayout()
layout.addWidget(self.source_line_edit, 0, 0)
layout.addWidget(copy_button, 0, 1)
layout.addWidget(self.dest_line_edit, 1, 0)
layout.addWidget(paste_button, 1, 1)
self.setLayout(layout)
def copy(self):
self.source_line_edit.selectAll()
self.source_line_edit.copy()
def paste(self):
self.dest_line_edit.clear()
self.dest_line_edit.paste()
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we create two line edits. The first one will hold some sample text. The second line edit will receive the text. Then, we create two buttons and connect their <code>clicked</code> signals to the <code>copy()</code> and <code>paste()</code> slots.</p>
<p>Inside the <code>copy()</code> method we first select all the text from the source line edit. Then we use the <code>copy()</code> method to copy the selected text to the clipboard. In <code>paste()</code>, we call <code>clear()</code> on the destination line edit to remove any previous text. Then, we use the <code>paste()</code> method to copy the clipboard's content.</p>
<p>Go ahead and run the application. You'll get the following window on your screen:</p>
<p><img alt="QLineEdit Copy and Paste" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-manipulation.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-manipulation.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with Copy & Paste buttons attached to handlers.</em></p>
<p>Once you've run the app, then you can click the <em>Copy</em> button to copy the text in the first line edit. Next, you can click the <em>Paste</em> button to paste the copied text to the second line edit. Go ahead and give it a try!</p>
<h2 id="aligning-and-formatting-the-text-in-a-line-edit">Aligning and Formatting the Text in a Line Edit</h2>
<p>You can also align and format the text in a line edit. For example, for text alignment, you can use the <a href="https://doc.qt.io/qt-6/qlineedit.html#alignment-prop"><code>setAlignment()</code></a> method with one or more alignment flags. Some of the most useful flags that you can find in the <a href="https://doc.qt.io/qt-6/qt.html#AlignmentFlag-enum"><code>Qt.AlignmentFlag</code></a> namespace includes the following:</p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AlignLeft</code></td>
<td>Aligns with the left edge.</td>
</tr>
<tr>
<td><code>AlignRight</code></td>
<td>Aligns with the right edge.</td>
</tr>
<tr>
<td><code>AlignHCenter</code></td>
<td>Centers horizontally in the available space.</td>
</tr>
<tr>
<td><code>AlignJustify</code></td>
<td>Justifies the text in the available space.</td>
</tr>
<tr>
<td><code>AlignTop</code></td>
<td>Aligns with the top.</td>
</tr>
<tr>
<td><code>AlignBottom</code></td>
<td>Aligns with the bottom.</td>
</tr>
<tr>
<td><code>AlignVCenter</code></td>
<td>Centers vertically in the available space.</td>
</tr>
<tr>
<td><code>AlignCenter</code></td>
<td>Centers in both dimensions.</td>
</tr>
</tbody>
</table>
<p>If you want to apply multiple alignment flags to a given line edit, you don't have to call <code>setAlignment()</code> multiple times. You can just use the bitwise OR operator (<code>|</code>) to combine them. Consider the following example:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLineEdit
class Window(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Aligning Text")
self.resize(300, 100)
self.setText("Hello, World!")
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we use a <code>QLineEdit</code> as the only component of our app's GUI. Using the <code>setAlignment()</code> method, we center the <code>"Hello, World!"</code> message in both directions, horizontally and vertically. To do this, we use the <code>AlignCenter</code> flag. Here's what the app looks like:</p>
<p><img alt="QLineEdit text alignment" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-alignment.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-alignment.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with text alignment set.</em></p>
<p>Go ahead and play with other flags to see their effect on the text alignment. Use the <code>|</code> bitwise operator to combine several alignment flags.</p>
<p>Line edits also have a <code>textMargins</code> property that you can tweak to customize the text alignment using specific values. To set margin values for your text, you can use the <code>setTextMargins()</code> method, which has the following signatures:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#setTextMargins"><code>setTextMargins(left, top, right, bottom)</code></a></td>
<td>Sets the margins around the text to have the sizes <code>left</code>, <code>top</code>, <code>right</code>, and <code>bottom</code>.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#setTextMargins-1"><code>setTextMargins(margins)</code></a></td>
<td>Sets the <code>margins</code> around the text using a <a href="https://doc.qt.io/qt-6/qmargins.html"><code>QMargins</code></a> object.</td>
</tr>
</tbody>
</table>
<p>The first signature allows you to provide four integer values as the left, top, right, and bottom margins for the text. The second signature accepts a <code>QMargins</code> object as an argument. To build this object, you can use four integer values with the same meaning as the <code>left</code>, <code>top</code>, <code>right</code>, and <code>bottom</code> arguments in the first signature.</p>
<p>Here's an example of how to set custom margins for the text in a line edit:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import QApplication, QLineEdit
class Window(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Aligning Text")
self.resize(300, 100)
self.setText("Hello, World!")
self.setTextMargins(30, 30, 0, 0)
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, you set the left and top margins to custom values. Here's how this app looks when you run it:</p>
<p><img alt="QLineEdit text margins" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-text-margins.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-text-margins.png?tr=w-600 600w" loading="lazy" width="600" height="256"/>
<em>QLineEdit with text margins added.</em></p>
<p>Using the <code>setTextMargins()</code> method, we can place the text in the desired place in a line edit, which may be a requirement in some situations.</p>
<h2 id="connecting-signals-and-slots">Connecting Signals and Slots</h2>
<p>When you're creating a GUI application and you need to use line edits, you may need to perform actions when the user enters or modifies the content of the line edit. To do this, you need to connect some of the signals of the line edit to specific slots or functions.</p>
<p>Depending on specific user events, the <code>QLineEdit</code> class can emit several different signals. Here's is a summary of these signals and their corresponding meaning:</p>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Emitted</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#textChanged"><code>textChanged(text)</code></a></td>
<td>Whenever the user changes the text either manually or programmatically. The <code>text</code> argument is the new text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#textEdited"><code>textEdited(text)</code></a></td>
<td>Whenever the user edits the text manually. The <code>text</code> argument is the new text.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#editingFinished"><code>editingFinished</code></a></td>
<td>When the user presses the <em>Return</em> or <em>Enter</em> key, or when the line edit loses focus, and its contents have changed since the last time this signal was emitted.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#inputRejected"><code>inputRejected</code></a></td>
<td>When the user presses a key that is an unacceptable input.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#returnPressed"><code>returnPressed</code></a></td>
<td>When the user presses the <em>Return</em> or <em>Enter</em> key.</td>
</tr>
<tr>
<td><a href="https://doc.qt.io/qt-6/qlineedit.html#selectionChanged"><code>selectionChanged</code></a></td>
<td>When the selection changes.</td>
</tr>
</tbody>
</table>
<p>We can connect either of these signals with any slot. A slot is a method or function that performs a concrete action in your application. We can connect a signal and a slot so that the slot gets called when the signal gets emitted. Here's the required syntax to do this:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">line_edit.<signal>.connect(<method>)
</code></pre>
</div>
<p>In this construct, <code>line_edit</code> is the <code>QLineEdit</code> object that we need to connect with a given slot. The <code><signal></code> placeholder can be any of the abovementioned signals. Finally, <code><method></code> represents the target slot or method.</p>
<p>Let's write an example that puts this syntax into action. For this example, we'll connect the <code>textEdited</code> signal with a method that updates the text of a <code>QLabel</code> to match the text of our line edit:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtWidgets import (
QApplication,
QLabel,
QLineEdit,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Signal and Slot")
self.resize(300, 100)
self.line_edit = QLineEdit(parent=self)
self.label = QLabel(parent=self)
self.line_edit.textEdited.connect(self.update_label)
layout = QVBoxLayout()
layout.addWidget(self.line_edit)
layout.addWidget(self.label)
self.setLayout(layout)
def update_label(self):
self.label.setText(self.line_edit.text())
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we connect the line edit's <code>textEdited</code> signal with the <code>update_label()</code> method, which sets the label's text to match the text we enter in our line edit. Go ahead and run the app. Then, enter some text in the line edit and see what happens with the label at the bottom of the app's window.</p>
<h2 id="validating-input-in-line-edits">Validating Input in Line Edits</h2>
<p>We can provide input validators to our line edits using the <a href="https://doc.qt.io/qt-6/qlineedit.html#setValidator"><code>setValidator()</code></a> method. This method takes a <code>QValidator</code> object as an argument. PyQt has a few built-in validators that you can use directly on a line edit:</p>
<ul>
<li><a href="https://doc.qt.io/qt-6/qintvalidator.html">QIntValidator</a> for integer validation</li>
<li><a href="https://doc.qt.io/qt-6/qdoublevalidator.html">QDoubleValidator</a> for floating-point validation</li>
<li><a href="https://doc.qt.io/qt-6/qregularexpressionvalidator.html">QRegularExpressionValidator</a> for validation based on regular expressions</li>
</ul>
<p>Validator objects process the input to check whether it's valid. In general, validator object has three possible states:</p>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>QValidator.State.Invalid</code></td>
<td><code>0</code></td>
<td>The input is invalid.</td>
</tr>
<tr>
<td><code>QValidator.State.Intermediate</code></td>
<td><code>1</code></td>
<td>The input is a valid intermediate value.</td>
</tr>
<tr>
<td><code>QValidator.State.Acceptable</code></td>
<td><code>2</code></td>
<td>The input is acceptable as a final result.</td>
</tr>
</tbody>
</table>
<p>When you set a validator to a given line edit, the user may change the content to any <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Intermediate</code></a> value during editing. However, they can't edit the text to a value that is <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Invalid</code></a>. The line edit will emit the <code>returnPressed</code> and <code>editingFinished</code> signals only when the validator validates input as <a href="https://doc.qt.io/qt-6/qvalidator.html#State-enum"><code>Acceptable</code></a>.</p>
<p>Here's an example where we use a <code>QIntValidator</code> and a <code>QRegularExpressionValidator</code></p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QIntValidator, QRegularExpressionValidator
from PyQt6.QtWidgets import (
QApplication,
QFormLayout,
QLabel,
QLineEdit,
QWidget,
)
class Window(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Input Validators")
self.resize(300, 100)
self.int_line_edit = QLineEdit(parent=self)
self.int_line_edit.setValidator(QIntValidator())
self.uppercase_line_edit = QLineEdit(parent=self)
input_validator = QRegularExpressionValidator(
QRegularExpression("[A-Z]+"), self.uppercase_line_edit
)
self.uppercase_line_edit.setValidator(input_validator)
self.uppercase_line_edit.returnPressed.connect(self.update_label)
self.signal_label = QLabel(parent=self)
layout = QFormLayout()
layout.addRow("Integers:", self.int_line_edit)
layout.addRow("Uppercase letters:", self.uppercase_line_edit)
layout.addRow("Signal:", self.signal_label)
self.setLayout(layout)
def update_label(self):
self.signal_label.setText("Return pressed!")
if __name__ == "__main__":
app = QApplication([])
window = Window()
window.show()
app.exec()
</code></pre>
</div>
<p>In this example, we have two line edits. In the first line edit, we've used a <code>QIntValidator</code> object. This way, the line edit will only accept valid integer numbers. If you try to type in something different, then the line edit won't accept your input.</p>
<p>In the second line edit, we've used a <code>QRegularExpressionValidator</code>. In this specific case, the regular expression accepts one or more uppercase letters. Again if you try to enter something else, then the line edit won't accept the input.</p>
<p>The <code>QLabel</code> will show the text <code>Return pressed!</code> if the input in the second line edit is valid and you press the Enter key. Here's what the app looks like:</p>
<p><img alt="QLineEdit with input validator" src="https://www.pythonguis.com/static/docs/qlineedit/qlineedit-input-validators.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/docs/qlineedit/qlineedit-input-validators.png?tr=w-600 600w" loading="lazy" width="600" height="292"/>
<em>QLineEdit with input validator.</em></p>
<p>Validators provide a quick way to restrict the user input and set our own validation rules. This way, we can ensure valid user input in our applications.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Line edits are pretty useful widgets in any GUI application. They allow text input through a single-line editor that has many cool features.</p>
<p>In this tutorial, you've learned how to create, use, and customize your line edits while building a GUI application with PyQt/PySide.</p>Plotting With PyQtGraph — Create Custom Plots in PyQt with PyQtGraph2024-01-15T06:00:00+00:002024-01-15T06:00:00+00:00John Limtag:www.pythonguis.com,2024-01-15:/tutorials/plotting-pyqtgraph/<p>One of the major fields where Python shines is in data science. For data exploration and cleaning, Python has many powerful tools, such as <a href="https://pandas.pydata.org/">pandas</a> and <a href="https://pypi.org/project/polar/">polar</a>. For visualization, Python has Matplotlib.</p>
<p>When you're building GUI applications with PyQt, you can have access to all those tools directly from within your app. While it is possible to embed <code>matplotlib</code> plots in PyQt, the experience doesn't feel entirely <em>native</em>. So, for highly integrated plots, you may want to consider using the <a href="https://www.pyqtgraph.org/">PyQtGraph</a> library instead.</p>
<p>PyQtGraph is built on top of Qt's native <code>QGraphicsScene</code>, so it gives better drawing performance, particularly for live data. It also provides interactivity and the ability to customize plots according to your needs.</p>
<p>In this tutorial, you'll learn the basics of creating plots with PyQtGraph. You'll also explore the different plot customization options, including background color, line colors, line type, axis labels, and more.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#installing-pyqtgraph">Installing PyQtGraph</a></li>
<li><a href="#creating-a-plotwidget-instance">Creating a PlotWidget Instance</a></li>
<li><a href="#customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</a><ul>
<li><a href="#background-color">Background Color</a></li>
<li><a href="#line-color-width-and-style">Line Color, Width, and Style</a></li>
<li><a href="#line-markers">Line Markers</a></li>
<li><a href="#plot-titles">Plot Titles</a></li>
<li><a href="#axis-labels">Axis Labels</a></li>
<li><a href="#plot-legends">Plot Legends</a></li>
<li><a href="#background-grid">Background Grid</a></li>
<li><a href="#axis-range">Axis Range</a></li>
</ul>
</li>
<li><a href="#multiple-plot-lines">Multiple Plot Lines</a></li>
<li><a href="#creating-dynamic-plots">Creating Dynamic Plots</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="installing-pyqtgraph">Installing PyQtGraph</h2>
<p>To use PyQtGraph with PyQt, you first need to install the library in your Python environment. You can do this using <code>pip</code> as follows:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m pip install pyqtgraph
</code></pre>
</div>
<p>Once the installation is complete, you will be able to import the module into your Python code. So, now you are ready to start creating plots.</p>
<h2 id="creating-a-plotwidget-instance">Creating a <code>PlotWidget</code> Instance</h2>
<p>In PyQtGraph, all plots use the <a href="https://pyqtgraph.readthedocs.io/en/latest/api_reference/widgets/plotwidget.html"><code>PlotWidget</code></a> class. This widget provides a <em>canvas</em> on which we can add and configure many types of plots. Under the hood, <code>PlotWidget</code> uses Qt's <code>QGraphicsScene</code> class, meaning that it's fast, efficient, and well-integrated with the rest of your app.</p>
<p>The code below shows a basic GUI app with a single <code>PlotWidget</code> in a <code>QMainWindow</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(time, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>In this short example, you create a PyQt app with a <code>PlotWidget</code> as its central widget. Then you create two lists of sample data for time and temperature. The final step to create the plot is to call the <code>plot()</code> methods with the data you want to visualize.</p>
<p>The first argument to <code>plot()</code> will be your <code>x</code> coordinate, while the second argument will be the <code>y</code> coordinate.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In all the examples in this tutorial, we import PyQtGraph using <code>import pyqtgraph as pg</code>. This is a common practice in PyQtGraph examples to keep things tidy and reduce typing.</p>
<p>If you run the above application, then you'll get the following window on your screen:</p>
<p><img alt="Basic PyQtGraph plot: Temperature vs time" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>Basic PyQtGraph plot: Temperature vs time.</em></p>
<p>PyQtGraph's default plot style is quite basic — a black background with a thin (barely visible) white line. Fortunately, the library provides several options that will allow us to deeply customize our plots.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In the examples in this tutorial, we'll create the PyQtGraph widget in code. To learn how to embed PyQtGraph plots when using Qt Designer, check out <a href="https://www.pythonguis.com/tutorials/embed-pyqtgraph-custom-widgets-qt-app/">Embedding custom widgets from Qt Designer</a>.</p>
<p>In the following section, we'll learn about the options we have available in PyQtGraph to improve the appearance and usability of our plots.</p>
<h2 id="customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</h2>
<p>Because PyQtGraph uses Qt's <code>QGraphicsScene</code> to render the graphs, we have access to all the standard Qt line and shape styling options for use in plots. PyQtGraph provides an <a href="https://en.wikipedia.org/wiki/API">API</a> for using these options to draw plots and manage the plot canvas.</p>
<p>Below, we'll explore the most common styling features that you'll need to create and customize your own plots with PyQtGraph.</p>
<h3 id="background-color">Background Color</h3>
<p>Beginning with the app skeleton above, we can change the background color by calling <code>setBackground()</code> on our <code>PlotWidget</code> instance, <code>self.graphWidget</code>. The code below sets the background to white by passing in the string <code>"w"</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Calling <code>setBackground()</code> with <code>"w"</code> as an argument changes the background of your plot to white, as you can see in the following window:</p>
<p><img alt="PyQtGraph plot with a white background" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a white background.</em></p>
<p>There are a number of colors available using single letters, as we did in the example above. They're based on the standard colors used in Matplotlib. Here are the most common codes:</p>
<table>
<thead>
<tr>
<th>Letter Code</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"b"</code></td>
<td>Blue</td>
</tr>
<tr>
<td><code>"c"</code></td>
<td>Cian</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Grey</td>
</tr>
<tr>
<td><code>"g"</code></td>
<td>Green</td>
</tr>
<tr>
<td><code>"k"</code></td>
<td>Black</td>
</tr>
<tr>
<td><code>"m"</code></td>
<td>Magenta</td>
</tr>
<tr>
<td><code>"r"</code></td>
<td>Red</td>
</tr>
<tr>
<td><code>"w"</code></td>
<td>White</td>
</tr>
<tr>
<td><code>"y"</code></td>
<td>Yellow</td>
</tr>
</tbody>
</table>
<p>In addition to these single-letter codes, we can create custom colors using the <a href="https://en.wikipedia.org/wiki/Web_colors#Hex_triplet">hexadecimal notation</a> as a string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground("#bbccaa") # Hex
</code></pre>
</div>
<p>We can also use <a href="https://en.wikipedia.org/wiki/RGB_color_model">RGB</a> and <a href="https://en.wikipedia.org/wiki/RGBA_color_model">RGBA</a> values passed in as 3-value and 4-value tuples, respectively. We must use values in the range from 0 to 255:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground((100, 50, 255)) # RGB each 0-255
self.plot_graph.setBackground((100, 50, 255, 25)) # RGBA (A = alpha opacity)
</code></pre>
</div>
<p>The first call to <code>setBackground()</code> takes a tuple representing an RGB color, while the second call takes a tuple representing an RGBA color.</p>
<p>We can also specify colors using Qt's <code>QColor</code> class if we prefer it:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtGui
# ...
self.plot_graph.setBackground(QtGui.QColor(100, 50, 254, 25))
</code></pre>
</div>
<p>Using <code>QColor</code> can be useful when you're using specific <code>QColor</code> objects elsewhere in your application and want to reuse them in your plots. For example, say that your app has a custom window background color, and you want to use it in the plots as well. Then you can do something like the following:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">color = self.palette().color(QtGui.QPalette.Window)
# ...
self.plot_graph.setBackground(color)
</code></pre>
</div>
<p>In the first line, you get the GUI's background color, while in the second line, you use that color for your plots.</p>
<h3 id="line-color-width-and-style">Line Color, Width, and Style</h3>
<p>Plot lines in PyQtGraph are drawn using the Qt <code>QPen</code> class. This gives us full control over line drawing, as we would have in any other <code>QGraphicsScene</code> drawing. To use a custom pen, you need to create a new <code>QPen</code> instance and pass it into the <code>plot()</code> method.</p>
<p>In the app below, we use a custom <code>QPen</code> object to change the line color to red:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature, pen=pen)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Here, we create a <code>QPen</code> object, passing in a 3-value tuple that defines an RGB red color. We could also define this color with the <code>"r"</code> code or with a <code>QColor</code> object. Then, we pass the pen to <code>plot()</code> with the <code>pen</code> argument.</p>
<p><img alt="PyQtGraph plot with a red plot line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red plot line.</em></p>
<p>By tweaking the <code>QPen</code> object, we can change the appearance of the line. For example, you can change the line width in pixels and the style (dashed, dotted, etc.), using Qt's line styles.</p>
<p>Update the following lines of code in your app to create a red, dashed line with 5 pixels of width:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtCore, QtWidgets
# ...
pen = pg.mkPen(color=(255, 0, 0), width=5, style=QtCore.Qt.DashLine)
</code></pre>
</div>
<p>The result of this code is shown below, giving a 5-pixel, dashed, red line:</p>
<p><img alt="PyQtGraph plot with a red, dashed, and 5-pixel line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red, dashed, and 5-pixel line</em></p>
<p>You can use all other Qt's line styles, including <code>Qt.SolidLine</code>, <code>Qt.DotLine</code>, <code>Qt.DashDotLine</code>, and <code>Qt.DashDotDotLine</code>. Examples of each of these lines are shown in the image below:</p>
<p><img alt="Qt Line Types" src="https://www.pythonguis.com/tutorials/pyqt6-plotting-pyqtgraph/Qt_Line_Types.png"/>
<em>Qt's line styles.</em></p>
<p>To learn more about Qt's line styles, check the <a href="https://doc.qt.io/qt-5/qpen.html#pen-style">documentation</a> about pen styles. There, you'll all you need to deeply customize the lines in your PyQtGraph plots.</p>
<h3 id="line-markers">Line Markers</h3>
<p>For many plots, it can be helpful to use point markers in addition or instead of lines on the plot. To draw a marker on your plot, pass the symbol you want to use as a marker when calling <code>plot()</code>. The following example uses the plus sign as a marker:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.plot(hour, temperature, symbol="+")
</code></pre>
</div>
<p>In this line of code, you pass a plus sign to the <code>symbol</code> argument. This tells PyQtGraph to use that symbol as a marker for the points in your plot.</p>
<p>If you use a custom <code>symbol</code>, then you can also use the <code>symbolSize</code>, <code>symbolBrush</code>, and <code>symbolPen</code> arguments to further customize the marker.</p>
<p>The value passed as <code>symbolBrush</code> can be any color, or <code>QBrush</code> instance, while <code>symbolPen</code> can be any color or a <code>QPen</code> instance. The pen is used to draw the shape, while the brush is used for the fill.</p>
<p>Go ahead and update your app's code to use a blue marker of size 15, on a red line:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.plot(
time,
temperature,
pen=pen,
symbol="+",
symbolSize=20,
symbolBrush="b",
)
</code></pre>
</div>
<p>In this code, you pass a plus sign to the <code>symbol</code> argument. You also customize the marker size and color. The resulting plot looks something like this:</p>
<p><img alt="PyQtGraph plot with a plus sign as a point marker" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a plus sign as a point marker.</em></p>
<p>In addition to the <code>+</code> plot marker, PyQtGraph supports the markers shown in the table below:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>Marker Shape</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"o"</code></td>
<td>Circle</td>
</tr>
<tr>
<td><code>"s"</code></td>
<td>Square</td>
</tr>
<tr>
<td><code>"t"</code></td>
<td>Triangle</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Diamond</td>
</tr>
<tr>
<td><code>"+"</code></td>
<td>Plus</td>
</tr>
<tr>
<td><code>"t1"</code></td>
<td>Triangle pointing upwards</td>
</tr>
<tr>
<td><code>"t2"</code></td>
<td>Triangle pointing right side</td>
</tr>
<tr>
<td><code>"t3"</code></td>
<td>Triangle pointing left side</td>
</tr>
<tr>
<td><code>"p"</code></td>
<td>Pentagon</td>
</tr>
<tr>
<td><code>"h"</code></td>
<td>Hexagon</td>
</tr>
<tr>
<td><code>"star"</code></td>
<td>Star</td>
</tr>
<tr>
<td><code>"x"</code></td>
<td>Cross</td>
</tr>
<tr>
<td><code>"arrow_up"</code></td>
<td>Arrow Up</td>
</tr>
<tr>
<td><code>"arrow_right"</code></td>
<td>Arrow Right</td>
</tr>
<tr>
<td><code>"arrow_down"</code></td>
<td>Arrow Down</td>
</tr>
<tr>
<td><code>"arrow_left"</code></td>
<td>Arrow Left</td>
</tr>
<tr>
<td><code>"crosshair"</code></td>
<td>Crosshair</td>
</tr>
</tbody>
</table>
<p>You can use any of these symbols as markers for your data points. If you have more specific marker requirements, then you can also use a <code>QPainterPath</code> object, which allows you to draw completely custom marker shapes.</p>
<h3 id="plot-titles">Plot Titles</h3>
<p>Plot titles are important to provide context around what is shown on a given chart. In PyQtGraph, you can add a main plot title using the <code>setTitle()</code> method on the <code>PlotWidget</code> object. Your title can be a regular Python string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time")
</code></pre>
</div>
<p>You can style your titles and change their font color and size by passing additional arguments to <code>setTitle()</code>. The code below sets the color to blue and the font size to 20 points:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
</code></pre>
</div>
<p>In this line of code, you set the title's font color to blue and the size to 20 points using the <code>color</code> and <code>size</code> arguments of <code>setTitle()</code>.</p>
<p>You could've even used CSS style and basic HTML tag syntax if you prefer, although it's less readable:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle(
'<span style="color: blue; font-size: 20pt">Temperature vs Time</span>'
)
</code></pre>
</div>
<p>In this case, you use a <code>span</code> HTML tag to wrap the title and apply some CSS styles on to of it. The final result is the same as suing the <code>color</code> and <code>size</code> arguments. Your plot will look like this:</p>
<p><img alt="PyQtGraph plot with title" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with title.</em></p>
<p>Your plot looks way better now. You can continue customizing it by adding informative lables to both axis.</p>
<h3 id="axis-labels">Axis Labels</h3>
<p>When it comes to axis labels, we can use the <code>setLabel()</code> method to create them. This method requires two arguments, <code>position</code> and <code>text</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel("left", "Temperature (°C)")
self.plot_graph.setLabel("bottom", "Time (min)")
</code></pre>
</div>
<p>The <code>position</code> argument can be any one of <code>"left"</code>, <code>"right"</code>, <code>"top"</code>, or <code>"bottom"</code>. They define the position of the axis on which the text is placed. The second argument, <code>text</code> is the text you want to use for the label.</p>
<p>You can pass an optional <code>style</code> argument into the <code>setLabel()</code> method. In this case, you need to use valid CSS name-value pairs. To provide these CSS pairs, you can use a dictionary:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
</code></pre>
</div>
<p>Here, you first create a dictionary containing CSS pairs. Then you pass this dictionary as an argument to the <code>setLabel()</code> method. Note that you need to use the dictionary unpacking operator to unpack the styles in the method call.</p>
<p>Again, you can use basic HTML syntax and CSS for the labels if you prefer:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel(
"left",
'<span style="color: red; font-size: 18px">Temperature (°C)</span>'
)
self.plot_graph.setLabel(
"bottom",
'<span style="color: red; font-size: 18px">Time (min)</span>'
)
</code></pre>
</div>
<p>This time, you've passed the styles in a <code>span</code> HTML tag with appropriate CSS styles. In either case, your plot will look something like this:</p>
<p><img alt="PyQtGraph plot with axis labels" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis labels.</em></p>
<p>Having axis labels highly improves the readability of your plots as you can see in the above example. So, it's a good practice that you should keep in mind when creating your plots.</p>
<h3 id="plot-legends">Plot Legends</h3>
<p>In addition to the axis labels and the plot title, you will often want to show a legend identifying what a given line represents. This feature is particularly important when you start adding multiple lines to a plot.</p>
<p>You can add a legend to a plot by calling the <code>addLegend()</code> method on the <code>PlotWidget</code> object. However, for this method to work, you need to provide a name for each line when calling <code>plot()</code>.</p>
<p>The example below assigns the name "Temperature Sensor" to the <code>plot()</code> method. This name will be used to identify the line in the legend:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.addLegend()
# ...
self.plot_graph.plot(
time,
temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
</code></pre>
</div>
<p>Note that you must call <code>addLegend()</code> before you call <code>plot()</code> for the legend to show up. Otherwise, the plot won't show the legend at all. Now your plot will look like the following:</p>
<p><img alt="PyQtGraph plot with legend" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with legend.</em></p>
<p>The legend appears in the top left by default. If you would like to move it, you can drag and drop the legend elsewhere. You can also specify a default offset by passing a 2-value tuple to the <code>offset</code> parameter when calling the <code>addLegend()</code> method. This will allow you to specify a custom position for the legend.</p>
<h3 id="background-grid">Background Grid</h3>
<p>Adding a background grid can make your plots easier to read, particularly when you're trying to compare relative values against each other. You can turn on the background grid for your plot by calling the <code>showGrid()</code> method on your <code>PlotWidget</code> instance. The method takes two Boolean arguments, <code>x</code> and <code>y</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.showGrid(x=True, y=True)
</code></pre>
</div>
<p>In this call to the <code>showGrid()</code> method, you enable the grid lines in both dimensions <code>x</code> and <code>y</code>. Here's how the plot looks now:</p>
<p><img alt="PyQtGraph plot with grid" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with grid.</em></p>
<p>You can toggle the <code>x</code> and <code>y</code> arguments independently, according to the dimension on which you want to enable the grid lines.</p>
<h3 id="axis-range">Axis Range</h3>
<p>Sometimes, it can be useful to predefine the range of values that is visible on the plot or to lock the axis to a consistent range regardless of the data input. In PyQtGraph, you can do this using the <code>setXRange()</code> and <code>setYRange()</code> methods. They force the plot to only show data within the specified ranges.</p>
<p>Below, we set two ranges, one on each axis. The first argument is the minimum value, and the second is the maximum:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
</code></pre>
</div>
<p>The first line of code sets the x-axis to show values between 1 and 10. The second line sets the y-axis to display values between 20 and 40. Here's how this changes the plot:</p>
<p><img alt="PyQtGraph plot with axis ranges" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis ranges</em></p>
<p>Now your plot looks more consistent. The axis show fix scales that are specifically set for the possible range of input data.</p>
<h2 id="multiple-plot-lines">Multiple Plot Lines</h2>
<p>It is common to have plots that involve more than one dependent variable. In PyQtGraph, you can plot multiple variables in a single chart by calling <code>.plot()</code> multiple times on the same <code>PlotWidget</code> instance.</p>
<p>In the following example, we plot temperatures values from two different sensors. We use the same line style but change the line color. To avoid code repetition, we define a new <code>plot_line()</code> method on our window:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature_1 = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
temperature_2 = [32, 35, 40, 22, 38, 32, 27, 38, 32, 38]
pen = pg.mkPen(color=(255, 0, 0))
self.plot_line(
"Temperature Sensor 1", time, temperature_1, pen, "b"
)
pen = pg.mkPen(color=(0, 0, 255))
self.plot_line(
"Temperature Sensor 2", time, temperature_2, pen, "r"
)
def plot_line(self, name, time, temperature, pen, brush):
self.plot_graph.plot(
time,
temperature,
name=name,
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush=brush,
)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The custom <code>plot_line()</code> method on the main window does the hard work. It accepts a <code>name</code> to set the line name for the plot legend. Then it takes the <code>time</code> and <code>temperature</code> arguments. The <code>pen</code> and <code>brush</code> arguments allow you to tweak other features of the lines.</p>
<p>To plot separate temperature values, we'll create a new list called <code>temperature_2</code> and populate it with random numbers similar to our old <code>temperature</code>, which now is <code>temperature_1</code>. Here's the plot looks now:</p>
<p><img alt="PyQtGrap plot with two lines" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGrap plot with two lines.</em></p>
<p>You can play around with the <code>plot_line()</code> method, customizing the markers, line widths, colors, and other parameters.</p>
<h2 id="creating-dynamic-plots">Creating Dynamic Plots</h2>
<p>You can also create dynamic plots with PyQtGraph. The <code>PlotWidget</code> can take new data and update the plot in real time without affecting other elements. To update a plot dynamically, we need a reference to the line object that the <code>plot()</code> method returns.</p>
<p>Once we have the reference to the plot line, we can call the <code>setData()</code> method on the line object to apply the new data. In the example below, we've adapted our temperature vs time plot to accept new temperature measures every minute. Note that we've set the timer to 300 milliseconds so that we don't have to wait an entire minute to see the updates:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from random import randint
import pyqtgraph as pg
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time dynamic plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setYRange(20, 40)
self.time = list(range(10))
self.temperature = [randint(20, 40) for _ in range(10)]
# Get a line reference
self.line = self.plot_graph.plot(
self.time,
self.temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
# Add a timer to simulate new temperature measurements
self.timer = QtCore.QTimer()
self.timer.setInterval(300)
self.timer.timeout.connect(self.update_plot)
self.timer.start()
def update_plot(self):
self.time = self.time[1:]
self.time.append(self.time[-1] + 1)
self.temperature = self.temperature[1:]
self.temperature.append(randint(20, 40))
self.line.setData(self.time, self.temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The first step to creating a dynamic plot is to get a reference to the plot line. In this example, we've used a <code>QTimer</code> object to set the measuring interval. We've connected the <code>update_plot()</code> method with the timer's <code>timeout</code> signal.</p>
<p>The<code>update_plot()</code> method does the work of updating the data in every interval. If you run the app, then you will see a plot with random data scrolling to the left:</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880128994?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>The time scale in the x-axis changes as the stream of data provides new values. You can replace the random data with your own real data. You can take the data from a live sensor readout, API, or from any other stream of data. PyQtGraph is performant enough to support multiple simultaneous dynamic plots using this technique.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, you've learned how to draw basic plots with PyQtGraph and customize plot components, such as lines, markers, titles, axis labels, and more. For a complete overview of PyQtGraph methods and capabilities, see the PyQtGraph <a href="https://pyqtgraph.readthedocs.io/en/latest/">documentation</a>. The PyQtGraph <a href="https://github.com/pyqtgraph/pyqtgraph">repository on Github</a> also has a complete set of plot <a href="https://github.com/pyqtgraph/pyqtgraph/tree/master/pyqtgraph/examples">examples</a>.</p><p>One of the major fields where Python shines is in data science. For data exploration and cleaning, Python has many powerful tools, such as <a href="https://pandas.pydata.org/">pandas</a> and <a href="https://pypi.org/project/polar/">polar</a>. For visualization, Python has Matplotlib.</p>
<p>When you're building GUI applications with PyQt, you can have access to all those tools directly from within your app. While it is possible to embed <code>matplotlib</code> plots in PyQt, the experience doesn't feel entirely <em>native</em>. So, for highly integrated plots, you may want to consider using the <a href="https://www.pyqtgraph.org/">PyQtGraph</a> library instead.</p>
<p>PyQtGraph is built on top of Qt's native <code>QGraphicsScene</code>, so it gives better drawing performance, particularly for live data. It also provides interactivity and the ability to customize plots according to your needs.</p>
<p>In this tutorial, you'll learn the basics of creating plots with PyQtGraph. You'll also explore the different plot customization options, including background color, line colors, line type, axis labels, and more.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#installing-pyqtgraph">Installing PyQtGraph</a></li>
<li><a href="#creating-a-plotwidget-instance">Creating a PlotWidget Instance</a></li>
<li><a href="#customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</a><ul>
<li><a href="#background-color">Background Color</a></li>
<li><a href="#line-color-width-and-style">Line Color, Width, and Style</a></li>
<li><a href="#line-markers">Line Markers</a></li>
<li><a href="#plot-titles">Plot Titles</a></li>
<li><a href="#axis-labels">Axis Labels</a></li>
<li><a href="#plot-legends">Plot Legends</a></li>
<li><a href="#background-grid">Background Grid</a></li>
<li><a href="#axis-range">Axis Range</a></li>
</ul>
</li>
<li><a href="#multiple-plot-lines">Multiple Plot Lines</a></li>
<li><a href="#creating-dynamic-plots">Creating Dynamic Plots</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="installing-pyqtgraph">Installing PyQtGraph</h2>
<p>To use PyQtGraph with PyQt, you first need to install the library in your Python environment. You can do this using <code>pip</code> as follows:</p>
<div class="code-block">
<span class="code-block-language code-block-sh">sh</span>
<pre><code class="sh">$ python -m pip install pyqtgraph
</code></pre>
</div>
<p>Once the installation is complete, you will be able to import the module into your Python code. So, now you are ready to start creating plots.</p>
<h2 id="creating-a-plotwidget-instance">Creating a <code>PlotWidget</code> Instance</h2>
<p>In PyQtGraph, all plots use the <a href="https://pyqtgraph.readthedocs.io/en/latest/api_reference/widgets/plotwidget.html"><code>PlotWidget</code></a> class. This widget provides a <em>canvas</em> on which we can add and configure many types of plots. Under the hood, <code>PlotWidget</code> uses Qt's <code>QGraphicsScene</code> class, meaning that it's fast, efficient, and well-integrated with the rest of your app.</p>
<p>The code below shows a basic GUI app with a single <code>PlotWidget</code> in a <code>QMainWindow</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
self.plot_graph.plot(time, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>In this short example, you create a PyQt app with a <code>PlotWidget</code> as its central widget. Then you create two lists of sample data for time and temperature. The final step to create the plot is to call the <code>plot()</code> methods with the data you want to visualize.</p>
<p>The first argument to <code>plot()</code> will be your <code>x</code> coordinate, while the second argument will be the <code>y</code> coordinate.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In all the examples in this tutorial, we import PyQtGraph using <code>import pyqtgraph as pg</code>. This is a common practice in PyQtGraph examples to keep things tidy and reduce typing.</p>
<p>If you run the above application, then you'll get the following window on your screen:</p>
<p><img alt="Basic PyQtGraph plot: Temperature vs time" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/basic-pyqtgraph-plot.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>Basic PyQtGraph plot: Temperature vs time.</em></p>
<p>PyQtGraph's default plot style is quite basic — a black background with a thin (barely visible) white line. Fortunately, the library provides several options that will allow us to deeply customize our plots.</p>
<p class="admonition admonition-info"><span class="admonition-kind"><i class="fas fa-info"></i></span> In the examples in this tutorial, we'll create the PyQtGraph widget in code. To learn how to embed PyQtGraph plots when using Qt Designer, check out <a href="https://www.pythonguis.com/tutorials/embed-pyqtgraph-custom-widgets-qt-app/">Embedding custom widgets from Qt Designer</a>.</p>
<p>In the following section, we'll learn about the options we have available in PyQtGraph to improve the appearance and usability of our plots.</p>
<h2 id="customizing-pyqtgraph-plots">Customizing PyQtGraph Plots</h2>
<p>Because PyQtGraph uses Qt's <code>QGraphicsScene</code> to render the graphs, we have access to all the standard Qt line and shape styling options for use in plots. PyQtGraph provides an <a href="https://en.wikipedia.org/wiki/API">API</a> for using these options to draw plots and manage the plot canvas.</p>
<p>Below, we'll explore the most common styling features that you'll need to create and customize your own plots with PyQtGraph.</p>
<h3 id="background-color">Background Color</h3>
<p>Beginning with the app skeleton above, we can change the background color by calling <code>setBackground()</code> on our <code>PlotWidget</code> instance, <code>self.graphWidget</code>. The code below sets the background to white by passing in the string <code>"w"</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">import pyqtgraph as pg
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Calling <code>setBackground()</code> with <code>"w"</code> as an argument changes the background of your plot to white, as you can see in the following window:</p>
<p><img alt="PyQtGraph plot with a white background" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-white-background.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a white background.</em></p>
<p>There are a number of colors available using single letters, as we did in the example above. They're based on the standard colors used in Matplotlib. Here are the most common codes:</p>
<table>
<thead>
<tr>
<th>Letter Code</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"b"</code></td>
<td>Blue</td>
</tr>
<tr>
<td><code>"c"</code></td>
<td>Cian</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Grey</td>
</tr>
<tr>
<td><code>"g"</code></td>
<td>Green</td>
</tr>
<tr>
<td><code>"k"</code></td>
<td>Black</td>
</tr>
<tr>
<td><code>"m"</code></td>
<td>Magenta</td>
</tr>
<tr>
<td><code>"r"</code></td>
<td>Red</td>
</tr>
<tr>
<td><code>"w"</code></td>
<td>White</td>
</tr>
<tr>
<td><code>"y"</code></td>
<td>Yellow</td>
</tr>
</tbody>
</table>
<p>In addition to these single-letter codes, we can create custom colors using the <a href="https://en.wikipedia.org/wiki/Web_colors#Hex_triplet">hexadecimal notation</a> as a string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground("#bbccaa") # Hex
</code></pre>
</div>
<p>We can also use <a href="https://en.wikipedia.org/wiki/RGB_color_model">RGB</a> and <a href="https://en.wikipedia.org/wiki/RGBA_color_model">RGBA</a> values passed in as 3-value and 4-value tuples, respectively. We must use values in the range from 0 to 255:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setBackground((100, 50, 255)) # RGB each 0-255
self.plot_graph.setBackground((100, 50, 255, 25)) # RGBA (A = alpha opacity)
</code></pre>
</div>
<p>The first call to <code>setBackground()</code> takes a tuple representing an RGB color, while the second call takes a tuple representing an RGBA color.</p>
<p>We can also specify colors using Qt's <code>QColor</code> class if we prefer it:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtGui
# ...
self.plot_graph.setBackground(QtGui.QColor(100, 50, 254, 25))
</code></pre>
</div>
<p>Using <code>QColor</code> can be useful when you're using specific <code>QColor</code> objects elsewhere in your application and want to reuse them in your plots. For example, say that your app has a custom window background color, and you want to use it in the plots as well. Then you can do something like the following:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">color = self.palette().color(QtGui.QPalette.Window)
# ...
self.plot_graph.setBackground(color)
</code></pre>
</div>
<p>In the first line, you get the GUI's background color, while in the second line, you use that color for your plots.</p>
<h3 id="line-color-width-and-style">Line Color, Width, and Style</h3>
<p>Plot lines in PyQtGraph are drawn using the Qt <code>QPen</code> class. This gives us full control over line drawing, as we would have in any other <code>QGraphicsScene</code> drawing. To use a custom pen, you need to create a new <code>QPen</code> instance and pass it into the <code>plot()</code> method.</p>
<p>In the app below, we use a custom <code>QPen</code> object to change the line color to red:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
self.plot_graph.plot(time, temperature, pen=pen)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>Here, we create a <code>QPen</code> object, passing in a 3-value tuple that defines an RGB red color. We could also define this color with the <code>"r"</code> code or with a <code>QColor</code> object. Then, we pass the pen to <code>plot()</code> with the <code>pen</code> argument.</p>
<p><img alt="PyQtGraph plot with a red plot line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red plot line.</em></p>
<p>By tweaking the <code>QPen</code> object, we can change the appearance of the line. For example, you can change the line width in pixels and the style (dashed, dotted, etc.), using Qt's line styles.</p>
<p>Update the following lines of code in your app to create a red, dashed line with 5 pixels of width:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtCore, QtWidgets
# ...
pen = pg.mkPen(color=(255, 0, 0), width=5, style=QtCore.Qt.DashLine)
</code></pre>
</div>
<p>The result of this code is shown below, giving a 5-pixel, dashed, red line:</p>
<p><img alt="PyQtGraph plot with a red, dashed, and 5-pixel line" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-red-dashed-plot-line.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a red, dashed, and 5-pixel line</em></p>
<p>You can use all other Qt's line styles, including <code>Qt.SolidLine</code>, <code>Qt.DotLine</code>, <code>Qt.DashDotLine</code>, and <code>Qt.DashDotDotLine</code>. Examples of each of these lines are shown in the image below:</p>
<p><img alt="Qt Line Types" src="https://www.pythonguis.com/tutorials/pyqt6-plotting-pyqtgraph/Qt_Line_Types.png"/>
<em>Qt's line styles.</em></p>
<p>To learn more about Qt's line styles, check the <a href="https://doc.qt.io/qt-5/qpen.html#pen-style">documentation</a> about pen styles. There, you'll all you need to deeply customize the lines in your PyQtGraph plots.</p>
<h3 id="line-markers">Line Markers</h3>
<p>For many plots, it can be helpful to use point markers in addition or instead of lines on the plot. To draw a marker on your plot, pass the symbol you want to use as a marker when calling <code>plot()</code>. The following example uses the plus sign as a marker:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.plot(hour, temperature, symbol="+")
</code></pre>
</div>
<p>In this line of code, you pass a plus sign to the <code>symbol</code> argument. This tells PyQtGraph to use that symbol as a marker for the points in your plot.</p>
<p>If you use a custom <code>symbol</code>, then you can also use the <code>symbolSize</code>, <code>symbolBrush</code>, and <code>symbolPen</code> arguments to further customize the marker.</p>
<p>The value passed as <code>symbolBrush</code> can be any color, or <code>QBrush</code> instance, while <code>symbolPen</code> can be any color or a <code>QPen</code> instance. The pen is used to draw the shape, while the brush is used for the fill.</p>
<p>Go ahead and update your app's code to use a blue marker of size 15, on a red line:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.plot(
time,
temperature,
pen=pen,
symbol="+",
symbolSize=20,
symbolBrush="b",
)
</code></pre>
</div>
<p>In this code, you pass a plus sign to the <code>symbol</code> argument. You also customize the marker size and color. The resulting plot looks something like this:</p>
<p><img alt="PyQtGraph plot with a plus sign as a point marker" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-point-marker.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with a plus sign as a point marker.</em></p>
<p>In addition to the <code>+</code> plot marker, PyQtGraph supports the markers shown in the table below:</p>
<table>
<thead>
<tr>
<th>Character</th>
<th>Marker Shape</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"o"</code></td>
<td>Circle</td>
</tr>
<tr>
<td><code>"s"</code></td>
<td>Square</td>
</tr>
<tr>
<td><code>"t"</code></td>
<td>Triangle</td>
</tr>
<tr>
<td><code>"d"</code></td>
<td>Diamond</td>
</tr>
<tr>
<td><code>"+"</code></td>
<td>Plus</td>
</tr>
<tr>
<td><code>"t1"</code></td>
<td>Triangle pointing upwards</td>
</tr>
<tr>
<td><code>"t2"</code></td>
<td>Triangle pointing right side</td>
</tr>
<tr>
<td><code>"t3"</code></td>
<td>Triangle pointing left side</td>
</tr>
<tr>
<td><code>"p"</code></td>
<td>Pentagon</td>
</tr>
<tr>
<td><code>"h"</code></td>
<td>Hexagon</td>
</tr>
<tr>
<td><code>"star"</code></td>
<td>Star</td>
</tr>
<tr>
<td><code>"x"</code></td>
<td>Cross</td>
</tr>
<tr>
<td><code>"arrow_up"</code></td>
<td>Arrow Up</td>
</tr>
<tr>
<td><code>"arrow_right"</code></td>
<td>Arrow Right</td>
</tr>
<tr>
<td><code>"arrow_down"</code></td>
<td>Arrow Down</td>
</tr>
<tr>
<td><code>"arrow_left"</code></td>
<td>Arrow Left</td>
</tr>
<tr>
<td><code>"crosshair"</code></td>
<td>Crosshair</td>
</tr>
</tbody>
</table>
<p>You can use any of these symbols as markers for your data points. If you have more specific marker requirements, then you can also use a <code>QPainterPath</code> object, which allows you to draw completely custom marker shapes.</p>
<h3 id="plot-titles">Plot Titles</h3>
<p>Plot titles are important to provide context around what is shown on a given chart. In PyQtGraph, you can add a main plot title using the <code>setTitle()</code> method on the <code>PlotWidget</code> object. Your title can be a regular Python string:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time")
</code></pre>
</div>
<p>You can style your titles and change their font color and size by passing additional arguments to <code>setTitle()</code>. The code below sets the color to blue and the font size to 20 points:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
</code></pre>
</div>
<p>In this line of code, you set the title's font color to blue and the size to 20 points using the <code>color</code> and <code>size</code> arguments of <code>setTitle()</code>.</p>
<p>You could've even used CSS style and basic HTML tag syntax if you prefer, although it's less readable:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setTitle(
'<span style="color: blue; font-size: 20pt">Temperature vs Time</span>'
)
</code></pre>
</div>
<p>In this case, you use a <code>span</code> HTML tag to wrap the title and apply some CSS styles on to of it. The final result is the same as suing the <code>color</code> and <code>size</code> arguments. Your plot will look like this:</p>
<p><img alt="PyQtGraph plot with title" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-title.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with title.</em></p>
<p>Your plot looks way better now. You can continue customizing it by adding informative lables to both axis.</p>
<h3 id="axis-labels">Axis Labels</h3>
<p>When it comes to axis labels, we can use the <code>setLabel()</code> method to create them. This method requires two arguments, <code>position</code> and <code>text</code>.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel("left", "Temperature (°C)")
self.plot_graph.setLabel("bottom", "Time (min)")
</code></pre>
</div>
<p>The <code>position</code> argument can be any one of <code>"left"</code>, <code>"right"</code>, <code>"top"</code>, or <code>"bottom"</code>. They define the position of the axis on which the text is placed. The second argument, <code>text</code> is the text you want to use for the label.</p>
<p>You can pass an optional <code>style</code> argument into the <code>setLabel()</code> method. In this case, you need to use valid CSS name-value pairs. To provide these CSS pairs, you can use a dictionary:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
</code></pre>
</div>
<p>Here, you first create a dictionary containing CSS pairs. Then you pass this dictionary as an argument to the <code>setLabel()</code> method. Note that you need to use the dictionary unpacking operator to unpack the styles in the method call.</p>
<p>Again, you can use basic HTML syntax and CSS for the labels if you prefer:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setLabel(
"left",
'<span style="color: red; font-size: 18px">Temperature (°C)</span>'
)
self.plot_graph.setLabel(
"bottom",
'<span style="color: red; font-size: 18px">Time (min)</span>'
)
</code></pre>
</div>
<p>This time, you've passed the styles in a <code>span</code> HTML tag with appropriate CSS styles. In either case, your plot will look something like this:</p>
<p><img alt="PyQtGraph plot with axis labels" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-labels.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis labels.</em></p>
<p>Having axis labels highly improves the readability of your plots as you can see in the above example. So, it's a good practice that you should keep in mind when creating your plots.</p>
<h3 id="plot-legends">Plot Legends</h3>
<p>In addition to the axis labels and the plot title, you will often want to show a legend identifying what a given line represents. This feature is particularly important when you start adding multiple lines to a plot.</p>
<p>You can add a legend to a plot by calling the <code>addLegend()</code> method on the <code>PlotWidget</code> object. However, for this method to work, you need to provide a name for each line when calling <code>plot()</code>.</p>
<p>The example below assigns the name "Temperature Sensor" to the <code>plot()</code> method. This name will be used to identify the line in the legend:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.addLegend()
# ...
self.plot_graph.plot(
time,
temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
</code></pre>
</div>
<p>Note that you must call <code>addLegend()</code> before you call <code>plot()</code> for the legend to show up. Otherwise, the plot won't show the legend at all. Now your plot will look like the following:</p>
<p><img alt="PyQtGraph plot with legend" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-legend.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with legend.</em></p>
<p>The legend appears in the top left by default. If you would like to move it, you can drag and drop the legend elsewhere. You can also specify a default offset by passing a 2-value tuple to the <code>offset</code> parameter when calling the <code>addLegend()</code> method. This will allow you to specify a custom position for the legend.</p>
<h3 id="background-grid">Background Grid</h3>
<p>Adding a background grid can make your plots easier to read, particularly when you're trying to compare relative values against each other. You can turn on the background grid for your plot by calling the <code>showGrid()</code> method on your <code>PlotWidget</code> instance. The method takes two Boolean arguments, <code>x</code> and <code>y</code>:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.showGrid(x=True, y=True)
</code></pre>
</div>
<p>In this call to the <code>showGrid()</code> method, you enable the grid lines in both dimensions <code>x</code> and <code>y</code>. Here's how the plot looks now:</p>
<p><img alt="PyQtGraph plot with grid" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-grid.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with grid.</em></p>
<p>You can toggle the <code>x</code> and <code>y</code> arguments independently, according to the dimension on which you want to enable the grid lines.</p>
<h3 id="axis-range">Axis Range</h3>
<p>Sometimes, it can be useful to predefine the range of values that is visible on the plot or to lock the axis to a consistent range regardless of the data input. In PyQtGraph, you can do this using the <code>setXRange()</code> and <code>setYRange()</code> methods. They force the plot to only show data within the specified ranges.</p>
<p>Below, we set two ranges, one on each axis. The first argument is the minimum value, and the second is the maximum:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
</code></pre>
</div>
<p>The first line of code sets the x-axis to show values between 1 and 10. The second line sets the y-axis to display values between 20 and 40. Here's how this changes the plot:</p>
<p><img alt="PyQtGraph plot with axis ranges" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-axis-ranges.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGraph plot with axis ranges</em></p>
<p>Now your plot looks more consistent. The axis show fix scales that are specifically set for the possible range of input data.</p>
<h2 id="multiple-plot-lines">Multiple Plot Lines</h2>
<p>It is common to have plots that involve more than one dependent variable. In PyQtGraph, you can plot multiple variables in a single chart by calling <code>.plot()</code> multiple times on the same <code>PlotWidget</code> instance.</p>
<p>In the following example, we plot temperatures values from two different sensors. We use the same line style but change the line color. To avoid code repetition, we define a new <code>plot_line()</code> method on our window:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt5 import QtWidgets
import pyqtgraph as pg
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setXRange(1, 10)
self.plot_graph.setYRange(20, 40)
time = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
temperature_1 = [30, 32, 34, 32, 33, 31, 29, 32, 35, 30]
temperature_2 = [32, 35, 40, 22, 38, 32, 27, 38, 32, 38]
pen = pg.mkPen(color=(255, 0, 0))
self.plot_line(
"Temperature Sensor 1", time, temperature_1, pen, "b"
)
pen = pg.mkPen(color=(0, 0, 255))
self.plot_line(
"Temperature Sensor 2", time, temperature_2, pen, "r"
)
def plot_line(self, name, time, temperature, pen, brush):
self.plot_graph.plot(
time,
temperature,
name=name,
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush=brush,
)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The custom <code>plot_line()</code> method on the main window does the hard work. It accepts a <code>name</code> to set the line name for the plot legend. Then it takes the <code>time</code> and <code>temperature</code> arguments. The <code>pen</code> and <code>brush</code> arguments allow you to tweak other features of the lines.</p>
<p>To plot separate temperature values, we'll create a new list called <code>temperature_2</code> and populate it with random numbers similar to our old <code>temperature</code>, which now is <code>temperature_1</code>. Here's the plot looks now:</p>
<p><img alt="PyQtGrap plot with two lines" src="https://www.pythonguis.com/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/graphics-plotting/plotting-pyqtgraph/pyqtgraph-plot-with-multiple-lines.png?tr=w-600 600w" loading="lazy" width="1280" height="1016"/>
<em>PyQtGrap plot with two lines.</em></p>
<p>You can play around with the <code>plot_line()</code> method, customizing the markers, line widths, colors, and other parameters.</p>
<h2 id="creating-dynamic-plots">Creating Dynamic Plots</h2>
<p>You can also create dynamic plots with PyQtGraph. The <code>PlotWidget</code> can take new data and update the plot in real time without affecting other elements. To update a plot dynamically, we need a reference to the line object that the <code>plot()</code> method returns.</p>
<p>Once we have the reference to the plot line, we can call the <code>setData()</code> method on the line object to apply the new data. In the example below, we've adapted our temperature vs time plot to accept new temperature measures every minute. Note that we've set the timer to 300 milliseconds so that we don't have to wait an entire minute to see the updates:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from random import randint
import pyqtgraph as pg
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Temperature vs time dynamic plot
self.plot_graph = pg.PlotWidget()
self.setCentralWidget(self.plot_graph)
self.plot_graph.setBackground("w")
pen = pg.mkPen(color=(255, 0, 0))
self.plot_graph.setTitle("Temperature vs Time", color="b", size="20pt")
styles = {"color": "red", "font-size": "18px"}
self.plot_graph.setLabel("left", "Temperature (°C)", **styles)
self.plot_graph.setLabel("bottom", "Time (min)", **styles)
self.plot_graph.addLegend()
self.plot_graph.showGrid(x=True, y=True)
self.plot_graph.setYRange(20, 40)
self.time = list(range(10))
self.temperature = [randint(20, 40) for _ in range(10)]
# Get a line reference
self.line = self.plot_graph.plot(
self.time,
self.temperature,
name="Temperature Sensor",
pen=pen,
symbol="+",
symbolSize=15,
symbolBrush="b",
)
# Add a timer to simulate new temperature measurements
self.timer = QtCore.QTimer()
self.timer.setInterval(300)
self.timer.timeout.connect(self.update_plot)
self.timer.start()
def update_plot(self):
self.time = self.time[1:]
self.time.append(self.time[-1] + 1)
self.temperature = self.temperature[1:]
self.temperature.append(randint(20, 40))
self.line.setData(self.time, self.temperature)
app = QtWidgets.QApplication([])
main = MainWindow()
main.show()
app.exec()
</code></pre>
</div>
<p>The first step to creating a dynamic plot is to get a reference to the plot line. In this example, we've used a <code>QTimer</code> object to set the measuring interval. We've connected the <code>update_plot()</code> method with the timer's <code>timeout</code> signal.</p>
<p>The<code>update_plot()</code> method does the work of updating the data in every interval. If you run the app, then you will see a plot with random data scrolling to the left:</p>
<p>
<div style="padding:79.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/880128994?badge=0&autoplay=1&loop=1&autopause=1&quality_selector=1&player_id=0&app_id=58479&background=1" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" style="position:absolute;top:0;left:0;width:100%;height:100%;" title="pyqtgraph-dynamic-plot"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
</p>
<p>The time scale in the x-axis changes as the stream of data provides new values. You can replace the random data with your own real data. You can take the data from a live sensor readout, API, or from any other stream of data. PyQtGraph is performant enough to support multiple simultaneous dynamic plots using this technique.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, you've learned how to draw basic plots with PyQtGraph and customize plot components, such as lines, markers, titles, axis labels, and more. For a complete overview of PyQtGraph methods and capabilities, see the PyQtGraph <a href="https://pyqtgraph.readthedocs.io/en/latest/">documentation</a>. The PyQtGraph <a href="https://github.com/pyqtgraph/pyqtgraph">repository on Github</a> also has a complete set of plot <a href="https://github.com/pyqtgraph/pyqtgraph/tree/master/pyqtgraph/examples">examples</a>.</p>How to Create a Custom Title Bar for a PyQt Window — Customize Your Python App's Title Bars2023-11-27T13:00:00+00:002023-11-27T13:00:00+00:00Leo Welltag:www.pythonguis.com,2023-11-27:/tutorials/custom-title-bar-pyqt6/<p>PyQt provides plenty of tools for creating unique and visually appealing graphical user interfaces (GUIs). One aspect of your applications that you may not have considered customizing is the <strong>title bar</strong>. The <em>title bar</em> is the topmost part of the window, where your users find the app's name, window controls & other elements.</p>
<p>This part of the window is usually drawn by the operating system or desktop environment and it's default look & feel may not gel well with the rest of your application. However, you may want to customize it to add additional functionality. For example, in web browsers the document tabs are now typically collapsed into the title bar to maximize available space for viewing pages.</p>
<p>In this tutorial, you will learn how to create custom title bars in PyQt. By the end of this tutorial, you will have the necessary knowledge to enhance your PyQt applications with personalized and (hopefully!) stylish title bars.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#creating-frameless-windows-in-pyqt">Creating Frameless Windows in PyQt</a></li>
<li><a href="#setting-up-the-main-window">Setting Up the Main Window</a></li>
<li><a href="#creating-a-custom-title-bar-for-a-pyqt-window">Creating a Custom Title Bar for a PyQt Window</a></li>
<li><a href="#updating-the-windows-state">Updating the Window's State</a></li>
<li><a href="#handling-windows-moves">Handling Window's Moves</a></li>
<li><a href="#making-it-a-little-more-beautiful">Making it a little more beautiful</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="creating-frameless-windows-in-pyqt">Creating Frameless Windows in PyQt</h2>
<p>The first step to providing a PyQt application with a custom <strong>title bar</strong> is to remove the default title bar and window decoration provided by the operating system. If we don't take this step, we'll end up with multiple title bars at the top of our windows.</p>
<p>In PyQt, we can create a frameless window using the <a href="https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop"><code>setWindowFlags()</code></a> method available on all <a href="https://doc.qt.io/qt-6/qwidget.html"><code>QWidget</code></a> subclasses, including <a href="https://doc.qt.io/qt-6/qmainwindow.html"><code>QMainWindow</code></a>. We call this method, passing in the <code>FramelessWindowHint</code> flag, which lives in the <code>Qt</code> namespace under the <code>WindowType</code> enumeration.</p>
<p>Here's the code of a minimal PyQt app whose main window is frameless:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<p>After importing the required classes, we create a window by subclassing <code>QMainWindow</code>. In the class initializer method, we set the window's title and resize the window using the <code>resize()</code> method. Then we use the <code>setWindowFlags()</code> to make the window frameless. The rest is the usual boilerplate code for creating PyQt applications.</p>
<p>If you run this app from your command line, you'll get the following window on your screen:</p>
<p><img alt="A frameless window in PyQt" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-600 600w" loading="lazy" width="1000" height="600"/>
<em>A frameless window in PyQt</em></p>
<p>As you can see, the app's main window doesn't have a title bar or any other decoration. It's only a gray rectangle on your screen.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Because the window has no buttons, you need to press <em>Alt-F4</em> on Windows and Linux or <em>Cmd+Q</em> on macOS to close the app.</p>
<p>This isn't very helpful, of course, but we'll be adding back in our custom title bar shortly.</p>
<h2 id="setting-up-the-main-window">Setting Up the Main Window</h2>
<p>Before creating our custom title bar, we'll finish the initialization of our app's main window, import some additional classes and create the window's central widget and layouts.</p>
<p>Here's the code update:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
central_widget = QWidget()
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
# ...
</code></pre>
</div>
<p>First, we import the <code>QLabel</code>, <code>QVBoxLayout</code>, and <code>QWidget</code> classes. In our window's initializer, we create a central widget by instantiating <code>QWidget()</code>. Next, we create an instance attribute called <code>title_bar</code> by instantiating a class called <code>CustomTitleBar</code>. We still need to implement this class -- we'll do this in a moment.</p>
<p>The next step is to create a layout for our window's workspace. In this example, we're using a <code>QVBoxLayout,</code> but you can use the layout that better fits your needs. We also set some margins for the layout content and added a label containing the phrase <code>"Hello, World!"</code>.</p>
<p>Next, we create a global layout for our central widget. Again, we use a <code>QVBoxLayout</code>. We set the layout's margins to <code>0</code> and aligned it on the top of our frameless window. In this layout, we need to add the title bar at the top and the workspace at the bottom. Finally, we set the central widget's layout and the app's central widget.</p>
<p>That's it! We have all the boilerplate code we need for our window to work correctly. Now we're ready to write our custom title bar.</p>
<h2 id="creating-a-custom-title-bar-for-a-pyqt-window">Creating a Custom Title Bar for a PyQt Window</h2>
<p>In this section, we will create a custom title bar for our main window. To do this, we will create a new class by inheriting from <code>QWidget</code>. First, go ahead and update your imports like in the code below:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
# ...
</code></pre>
</div>
<p>Here, we've imported a few new classes. We will use these classes as building blocks for our title bar. Without further ado, let's get into the title bar code. We'll introduce the code in small consecutive chunks to facilitate the explanation. Here's the first piece:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.ColorRole.Highlight)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
</code></pre>
</div>
<p>In this code snippet, we create a new class by inheriting from <code>QWidget</code>. This way, our title bar will have all the standard features and functionalities of any PyQt widgets. In the class initializer, we set <code>autoFillBackground</code> to true because we want to give a custom color to the bar. The next line of code sets the title bar's background color to <code>QPalette.ColorRole.Highlight</code>, which is a blueish color.</p>
<p>The next line of code creates and initializes an instance attribute called <code>initial_pos</code>. We'll use this attribute later on when we deal with moving the window around our screen.</p>
<p>The final three lines of code allow us to create a layout for our title bar. Because the title bar should be horizontally oriented, we use a <code>QHBoxLayout</code> class to structure it.</p>
<p>The piece of code below deals with our window's title:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setStyleSheet(
"""font-weight: bold;
border: 2px solid black;
border-radius: 12px;
margin: 2px;
"""
)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
</code></pre>
</div>
<p>The first line of new code creates a <code>title</code> attribute. It's a <code>QLable</code> object that will hold the window's title. Because we want to build a cool title bar, we'd like to add some custom styling to the title. To do this, we use the <code>setStyleSheet()</code> method with a string representing a CSS style sheet as an argument. The style sheet tweaks the font, borders, and margins of our title label.</p>
<p>Next, we center the title using the <code>setAlignment()</code> method with the <code>Qt.AlignmentFlag.AlignCenter</code> flag as an argument.</p>
<p>In the conditional statement, we check whether our window has a title. If that's the case, we set the text of our <code>title</code> label to the current window's title. Finally, we added the <code>title</code> label to the title bar layout.</p>
<p>The next step in our journey to build a custom title bar is to provide standard window controls. In other words, we need to add the <em>minimize</em>, <em>maximize</em>, <em>close</em>, and <em>normal</em> buttons. These buttons will allow our users to interact with our window. To create the buttons, we'll use the <code>QToolButton</code> class.</p>
<p>Here's the required code:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
# Min button
self.min_button = QToolButton(self)
min_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMinButton
)
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMaxButton
)
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarCloseButton
)
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarNormalButton
)
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
</code></pre>
</div>
<p>In this code snippet, we define all the required buttons by instantiating the <code>QToolButton</code> class. The <em>minimize</em>, <em>maximize</em>, and <em>close</em> buttons follow the same pattern. We create the button, define an icon for the buttons at hand, and set the icon using the <code>setIcon()</code> method.</p>
<p>Note that we use the standard icons that PyQt provides. For example, the <em>minimize</em> button uses the <code>SP_TitleBarMinButton</code> icon. Similarly, the <em>maximize</em> and <em>close</em> buttons use the <code>SP_TitleBarMaxButton</code> and <code>SP_TitleBarCloseButton</code> icons. We find all these icons in the <code>QStyle.StandardPixmap</code> namespace.</p>
<p>Finally, we connect the button's <code>clicked()</code> signal with the appropriate slot. For the minimize buttons, the proper slot is <code>.showMinimized()</code>. For the maximize and close buttons, the right slots are <code>.showMaximized()</code> and <code>close()</code>, respectively. All these slots are part of the main window's class.</p>
<p>The <em>normal</em> button at the end of the above code uses the <code>SP_TitleBarNormalButton</code> icon and <code>showNormal()</code> slot. This button has an extra setting. We've set its visibility to <code>False</code>, meaning that the button will be hidden by default. It'll only appear when we maximize the window to allow us to return to the normal state.</p>
<p>Now that we've created and tweaked the buttons, we must add them to our title bar. To do this, we can use the following <code>for</code> loop:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(28, 28))
button.setStyleSheet(
"""QToolButton { border: 2px solid white;
border-radius: 12px;
}
"""
)
title_bar_layout.addWidget(button)
</code></pre>
</div>
<p>This loop iterates over our four buttons in a predefined order. The first thing to do inside the loop is to define the focus policy of each button. We don't want these buttons to steal focus from buttons in the window's working space , therefore we set their focus policy to <code>NoFocus</code>.</p>
<p>Next, we set a fixed size of 28 by 28 pixels for the three buttons using the <code>setFixedSize()</code> method with a <code>QSize</code> object as an argument.</p>
<p>Our main goal in this section is to create a custom title bar. A handy way to customize the look and feel of PyQt widgets is to use CSS style sheets. In the above piece of code, we use the <code>setStyleSheet()</code> method to apply a custom CSS style sheet to our four buttons. The sheet defines a white and round border for each button.</p>
<p>The final line in the above code calls the <code>addWidget()</code> method to add each custom button to our title bar's layout. That's it! We're now ready to give our title bar a try. Go ahead and run the application from your command line. You'll see a window like the following:</p>
<p><img alt="A PyQt window with a custom title bar" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-600 600w" loading="lazy" width="1000" height="600"/>
<em>A PyQt window with a custom title bar</em></p>
<p>This is pretty simple styling, but you get the idea. You can tweak the title bar further, depending on your needs. For example, you can change the colors and borders, customize the title's font, add other widgets to the bar, and more.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We'll apply some nicer styles later, once we have the functionality in place! Keep reading.</p>
<p>Even though the title bar looks different, it has limited functionality. For example, if you click the <em>maximize</em> button, then the window will change to its maximized state. However, the <em>normal</em> button won't show up to allow you to return the window to its previous state.</p>
<p>In addition to this, if you try to move the window around your screen, you'll quickly notice a problem: it's impossible to move the window!</p>
<p>In the following sections, we'll write the necessary code to fix these issues and make our custom title bar fully functional. To kick things off, let's start by fixing the state issues.</p>
<h2 id="updating-the-windows-state">Updating the Window's State</h2>
<p>To fix the issue related to the window's state, we'll write two new methods. We need to override one method and write another. In the <code>MainWindow</code> class, we'll override the <code>changeEvent()</code> method. The <code>changeEvent()</code> method is called directly by Qt whenever the window state changes: for example if the window is maximized or hidden. By overriding this event we can add our own custom behavior.</p>
<p>Here's the code that overrides the <code>changeEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt, QEvent
# ...
class MainWindow(QMainWindow):
# ...
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
</code></pre>
</div>
<p>This method is fairly straightforward. We check the event type to see if it is a <code>WindowStateChange</code>. If that's the case, we call the <code>window_state_changed()</code> method of our custom title bar, passing the current window's state as an argument. In the final two lines, we call the parent class's <code>changeEvent()</code> method and accept the event to signal that we've correctly processed it.</p>
<p>Here's the implementation of our <code>window_state_changed()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
</code></pre>
</div>
<p>This method takes a window's state as an argument. Depending on the value of the state parameter we will optionally show and hide the maximize & restore buttons.</p>
<p>First, if the window is currently maximized we will show the <em>normal</em> button and hide the <em>maximize</em> button. Alternatively, if the window is currently <em>not</em> maximized we will hide the <em>normal</em> button and show the <em>maximize</em> button.</p>
<p>The effect of this, together with the order we added the buttons above, is that when you maximize the window the <em>maximize</em> button will <em>appear to be</em> replaced with the <em>normal</em> button. When you restore the window to it's normal size, the <em>normal</em> button will be replaced with the <em>maximize</em> button.</p>
<p>Go ahead and run the app again. Click the <em>maximize</em> button. You'll note that when the window gets maximized, the middle button changes its icon. Now you have access to the <em>normal</em> button. If you click it, then the window will recover its previous state.</p>
<h2 id="handling-windows-moves">Handling Window's Moves</h2>
<p>Now it's time to write the code that enables us to move the window around the screen while holding your mouse's left-click button on the title bar. To fix this issue, we only need to add code to the <code>CustomTitleBar</code> class.</p>
<p>In particular, we need to override three mouse events:</p>
<ul>
<li><code>mousePressEvent()</code> will let us know when the user clicks on our custom title bar using the mouse's left-click button. This may indicate that the window movement should start.</li>
<li><code>mouseMoveEvent()</code> will let us process the window movements.</li>
<li><code>mouseReleaseEvent()</code> will let us know when the user has released the mouse's left-click button so that we can stop moving the window.</li>
</ul>
<p>Here's the code that overrides the <code>mousePressEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
</code></pre>
</div>
<p>In this method, we first check if the user clicks on the title bar using the mouse's left-click button. If that's the case, then we update our <code>initial_pos</code> attribute to the clicked point. Remember that we defined <code>initial_pos</code> and initialized it to <code>None</code> back in the <code>__init__()</code> method of <code>CustomTitleBar</code>.</p>
<p>Next, we need to override the <code>mousePressEvent()</code> method. Here's the required code:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
</code></pre>
</div>
<p>This <code>if</code> statement in <code>mouseMoveEvent()</code> checks if the <code>initial_pos</code> attribute is not <code>None</code>. If this condition is true, then the <code>if</code> code block executes because we have a valid initial position.</p>
<p>The first line in the <code>if</code> code block calculates the difference, or<code>delta</code>, between the current and initial mouse positions. To get the current position, we call the <code>position()</code> method on the <code>event</code> object and convert that position into a <code>QPoint</code> object using the <code>toPoint()</code> method.</p>
<p>The following four lines update the position of our application's main window by adding the <code>delta</code> values to the current window position. The <code>move()</code> method does the hard work of moving the window.</p>
<p>In summary, this code updates the window position based on the movement of our mouse. It tracks the initial position of the mouse, calculates the difference between the initial position and the current position, and applies that difference to the window's position.</p>
<p>Finally, we can complete the <code>mouseReleaseEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
</code></pre>
</div>
<p>This method's implementation is pretty straightforward. Its purpose is to reset the initial position by setting it back to <code>None</code> when the mouse is released, indicating that the drag is complete.</p>
<p>That's it! Go ahead and run your app again. Click on your custom title bar and move the window around while holding the mouse's left-click button. Can you move the window? Great! Your custom title bar is now fully functional.</p>
<p>The completed code for the custom title bar is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt, QEvent
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.ColorRole.Highlight)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setStyleSheet(
"""font-weight: bold;
border: 2px solid black;
border-radius: 12px;
margin: 2px;
"""
)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
# Min button
self.min_button = QToolButton(self)
min_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMinButton
)
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMaxButton
)
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarCloseButton
)
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarNormalButton
)
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# Add buttons
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(28, 28))
button.setStyleSheet(
"""QToolButton { border: 2px solid white;
border-radius: 12px;
}
"""
)
title_bar_layout.addWidget(button)
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
central_widget = QWidget()
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
def window_state_changed(self, state):
self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<h2 id="making-it-a-little-more-beautiful">Making it a little more beautiful</h2>
<p>So far we've covered the technical aspects of styling our window with a custom title bar, and added the code to make it function as expected. But it doesn't look <em>great</em>. In this section we'll take our existing code & tweak the styling and buttons to produce something that's a little more professional looking.</p>
<p>One common reason for wanting to apply custom title bars to a window is to integrate the title bar with the rest of the application. This technique is called a <em>unified title bar</em> and can be seen in some popular applications such as web browsers, or Spotify.</p>
<p><img alt="Unified title bar in Spotify" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-600 600w" loading="lazy" width="534" height="334"/></p>
<p>In this section we'll look at how we can reproduce the same effect in PyQt using a combination of stylesheets & icons. Below is a screenshot of the final result which we'll be building.</p>
<p><img alt="Style custom title bar in PyQt6" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-600 600w" loading="lazy" width="1018" height="588"/></p>
<p>As you can see the window & the toolbar blend nicely together and the window has rounded corners. There are a few different ways to do this, but we'll cover a simple approach using Qt stylesheets to apply styling over the entire window.</p>
<p>In order to customize the shape of the window, we need to first tell the OS to stop drawing the default window outline and background for us. We do that by setting a <em>window attribute</em> on the window. This is similar to the flags we already discussed, in that it turns on & off different window manager behaviors.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# ...
</code></pre>
</div>
<p>We've added a call to <code>self.setAttribute</code> which sets the attribute <code>Qt.WidgetAttribute.WA_TranslucentBackground</code> on the window. If you run the code now you will see the window has become transparent, with only the widget text & toolbar visible.</p>
<p>Next we'll tell Qt to draw a new <em>custom</em> background for us. If you've worked with QSS before, the most obvious way to apply curved edges to the window using QSS stylesheets would be to set <code>border-radius:</code> styles on the main window directly, e.g.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">#...
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# ...
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setStyleSheet("background-color: gray; border-radius: 10px;")
#...
</code></pre>
</div>
<p>However, if you try this you'll notice that it doesn't work. If you enable a translucent background, the background of the window is not drawn (including your styles). If you <em>don't</em> set translucent background, the window is filled to the edges with a solid color ignoring the border radius.</p>
<p><img alt="Stylesheets can't alter window shape" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-600 600w" loading="lazy" width="1850" height="608"/>.</p>
<p>The good news is that, with a bit of lateral thinking, there is a simple solution. We already know that we can construct interfaces by nesting widgets in layouts. Since we can't style the <code>border-radius</code> of a window, but we <em>can</em> style any other widget, the solution is to simply add a container widget into our window & apply the curved-edge and background styles to that.</p>
<p>On our <code>MainWindow</code> object we already have a <em>central widget</em> which contains our layout, so we can apply the styles there.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# ...
central_widget = QWidget()
# This container holds the window contents, so we can style it.
central_widget.setObjectName("Container")
central_widget.setStyleSheet("""#Container {
background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
border-radius: 5px;
}""")
self.title_bar = CustomTitleBar(self)
# ...
</code></pre>
</div>
<p>We've taken the existing <code>central_widget</code> object and assigned an <em>object name</em> to it. This is a ID which we can use to refer to the widget from QSS, to apply our styles specifically to that widget.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> If you're familiar with CSS you might expect that IDs like <code>#Container</code> must be unique. However, they are not: you can give multiple widgets the same object name if you like. So you can re-use this technique and QSS on multiple windows in your application without problems.</p>
<p>With this style applied on our window, we have a nice gradient background with curved corners.</p>
<p>Unfortunately, the title bar we created is drawn filled, and so the background and curved corners of our window are over-written. To make things look coherent we need to make our title bar also transparent by removing the background color & auto-fill behavior we set earlier.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We don't need to set any flags or attributes this widget because it is not a window. A <code>QWidget</code> object is transparent by default.</p>
<p>We can also make some tweaks to the style of the title label, such as adjusting the font size and making the title capitalized using <code>text-transform: uppercase</code> -- feel free to customize this yourself.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# self.setAutoFillBackground(True) # <-- remove
# self.setBackgroundRole(QPalette.ColorRole.Highlight) # <-- remove
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setStyleSheet("""
QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
""")
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> QSS is <em>very</em> similar to CSS, especially for text styling.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The <code>margin-left: 48px</code> is to compensate for the 3 * 16px window icons on the right hand side so the text align centrally.</p>
<p>The icons are currently using built-in Qt icons which are a little bit plain & ugly. Next let's update the icons, using custom SVG icons of simple colored circles, for the minimize, maximize, close & restore buttons.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtGui import QIcon
# ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# ...
# Min button
self.min_button = QToolButton(self)
min_icon = QIcon()
min_icon.addFile('min.svg')
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = QIcon()
max_icon.addFile('max.svg')
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = QIcon()
close_icon.addFile('close.svg') # Close has only a single state.
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = QIcon()
normal_icon.addFile('normal.svg')
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# ...
</code></pre>
</div>
<p>This code follows the same basic structure as before, but instead of using the built-in icons here we're loading our icons from SVG images. These images are very simple, consisting of a single circle in green, red or yellow for the different states mimicking macOS.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The <code>normal.svg</code> file for returning a maximized window to normal size shows a semi-transparent green circle for simplicity's sake, but you can include iconography and hover behaviors on the buttons if you prefer.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can download these icons & all source code for this tutorial here: https://downloads.pythonguis.com/custom-title-bar-pyqt6.zip</p>
<p>The final step is to iterate through the created buttons, adding them to title bar layout. This is slightly tweaked from before to remove the border styling replacing it with simple padding & setting the icon sizes to 16px. Because we are using SVG files the icons will automatically scale to the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# ...
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(16, 16))
button.setStyleSheet(
"""QToolButton {
border: none;
padding: 2px;
}
"""
)
title_bar_layout.addWidget(button)
</code></pre>
</div>
<p>And that's it! With these changes, you can now run your application and you'll see a nice sleek modern-looking UI with unified title bar and custom controls.</p>
<p><img alt="Style custom titlebar in PyQt6" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-600 600w" loading="lazy" width="1748" height="1052"/>
<em>The final result, showing our unified title bar and window design.</em></p>
<p>The complete code is shown below:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QEvent, QSize, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setStyleSheet(
"""
QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
"""
)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
# Min button
self.min_button = QToolButton(self)
min_icon = QIcon()
min_icon.addFile("min.svg")
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = QIcon()
max_icon.addFile("max.svg")
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = QIcon()
close_icon.addFile("close.svg") # Close has only a single state.
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = QIcon()
normal_icon.addFile("normal.svg")
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# Add buttons
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(16, 16))
button.setStyleSheet(
"""QToolButton {
border: none;
padding: 2px;
}
"""
)
title_bar_layout.addWidget(button)
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
central_widget = QWidget()
# This container holds the window contents, so we can style it.
central_widget.setObjectName("Container")
central_widget.setStyleSheet(
"""#Container {
background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
border-radius: 5px;
}"""
)
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
def window_state_changed(self, state):
self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, we have learned the fundamentals of creating custom title bars in PyQt. To do this, we have combined PyQt's widgets, layouts, and styling capabilities to create a visually appealing title bar for a PyQt app.</p>
<p>With this skill under your belt, you're now ready to create title bars that align perfectly with your application's unique style and branding. This will allow you to break away from the standard window decoration provided by your operating system and add a personal touch to your user interface.</p>
<p>Now let your imagination run and transform your PyQt application's UX.</p><p>PyQt provides plenty of tools for creating unique and visually appealing graphical user interfaces (GUIs). One aspect of your applications that you may not have considered customizing is the <strong>title bar</strong>. The <em>title bar</em> is the topmost part of the window, where your users find the app's name, window controls & other elements.</p>
<p>This part of the window is usually drawn by the operating system or desktop environment and it's default look & feel may not gel well with the rest of your application. However, you may want to customize it to add additional functionality. For example, in web browsers the document tabs are now typically collapsed into the title bar to maximize available space for viewing pages.</p>
<p>In this tutorial, you will learn how to create custom title bars in PyQt. By the end of this tutorial, you will have the necessary knowledge to enhance your PyQt applications with personalized and (hopefully!) stylish title bars.</p>
<div class="toc"><span class="toctitle">Table of Contents</span><ul>
<li><a href="#creating-frameless-windows-in-pyqt">Creating Frameless Windows in PyQt</a></li>
<li><a href="#setting-up-the-main-window">Setting Up the Main Window</a></li>
<li><a href="#creating-a-custom-title-bar-for-a-pyqt-window">Creating a Custom Title Bar for a PyQt Window</a></li>
<li><a href="#updating-the-windows-state">Updating the Window's State</a></li>
<li><a href="#handling-windows-moves">Handling Window's Moves</a></li>
<li><a href="#making-it-a-little-more-beautiful">Making it a little more beautiful</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<h2 id="creating-frameless-windows-in-pyqt">Creating Frameless Windows in PyQt</h2>
<p>The first step to providing a PyQt application with a custom <strong>title bar</strong> is to remove the default title bar and window decoration provided by the operating system. If we don't take this step, we'll end up with multiple title bars at the top of our windows.</p>
<p>In PyQt, we can create a frameless window using the <a href="https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop"><code>setWindowFlags()</code></a> method available on all <a href="https://doc.qt.io/qt-6/qwidget.html"><code>QWidget</code></a> subclasses, including <a href="https://doc.qt.io/qt-6/qmainwindow.html"><code>QMainWindow</code></a>. We call this method, passing in the <code>FramelessWindowHint</code> flag, which lives in the <code>Qt</code> namespace under the <code>WindowType</code> enumeration.</p>
<p>Here's the code of a minimal PyQt app whose main window is frameless:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<p>After importing the required classes, we create a window by subclassing <code>QMainWindow</code>. In the class initializer method, we set the window's title and resize the window using the <code>resize()</code> method. Then we use the <code>setWindowFlags()</code> to make the window frameless. The rest is the usual boilerplate code for creating PyQt applications.</p>
<p>If you run this app from your command line, you'll get the following window on your screen:</p>
<p><img alt="A frameless window in PyQt" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/frameless-window-pyqt.png?tr=w-600 600w" loading="lazy" width="1000" height="600"/>
<em>A frameless window in PyQt</em></p>
<p>As you can see, the app's main window doesn't have a title bar or any other decoration. It's only a gray rectangle on your screen.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> Because the window has no buttons, you need to press <em>Alt-F4</em> on Windows and Linux or <em>Cmd+Q</em> on macOS to close the app.</p>
<p>This isn't very helpful, of course, but we'll be adding back in our custom title bar shortly.</p>
<h2 id="setting-up-the-main-window">Setting Up the Main Window</h2>
<p>Before creating our custom title bar, we'll finish the initialization of our app's main window, import some additional classes and create the window's central widget and layouts.</p>
<p>Here's the code update:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
central_widget = QWidget()
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
# ...
</code></pre>
</div>
<p>First, we import the <code>QLabel</code>, <code>QVBoxLayout</code>, and <code>QWidget</code> classes. In our window's initializer, we create a central widget by instantiating <code>QWidget()</code>. Next, we create an instance attribute called <code>title_bar</code> by instantiating a class called <code>CustomTitleBar</code>. We still need to implement this class -- we'll do this in a moment.</p>
<p>The next step is to create a layout for our window's workspace. In this example, we're using a <code>QVBoxLayout,</code> but you can use the layout that better fits your needs. We also set some margins for the layout content and added a label containing the phrase <code>"Hello, World!"</code>.</p>
<p>Next, we create a global layout for our central widget. Again, we use a <code>QVBoxLayout</code>. We set the layout's margins to <code>0</code> and aligned it on the top of our frameless window. In this layout, we need to add the title bar at the top and the workspace at the bottom. Finally, we set the central widget's layout and the app's central widget.</p>
<p>That's it! We have all the boilerplate code we need for our window to work correctly. Now we're ready to write our custom title bar.</p>
<h2 id="creating-a-custom-title-bar-for-a-pyqt-window">Creating a Custom Title Bar for a PyQt Window</h2>
<p>In this section, we will create a custom title bar for our main window. To do this, we will create a new class by inheriting from <code>QWidget</code>. First, go ahead and update your imports like in the code below:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
# ...
</code></pre>
</div>
<p>Here, we've imported a few new classes. We will use these classes as building blocks for our title bar. Without further ado, let's get into the title bar code. We'll introduce the code in small consecutive chunks to facilitate the explanation. Here's the first piece:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.ColorRole.Highlight)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
</code></pre>
</div>
<p>In this code snippet, we create a new class by inheriting from <code>QWidget</code>. This way, our title bar will have all the standard features and functionalities of any PyQt widgets. In the class initializer, we set <code>autoFillBackground</code> to true because we want to give a custom color to the bar. The next line of code sets the title bar's background color to <code>QPalette.ColorRole.Highlight</code>, which is a blueish color.</p>
<p>The next line of code creates and initializes an instance attribute called <code>initial_pos</code>. We'll use this attribute later on when we deal with moving the window around our screen.</p>
<p>The final three lines of code allow us to create a layout for our title bar. Because the title bar should be horizontally oriented, we use a <code>QHBoxLayout</code> class to structure it.</p>
<p>The piece of code below deals with our window's title:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setStyleSheet(
"""font-weight: bold;
border: 2px solid black;
border-radius: 12px;
margin: 2px;
"""
)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
</code></pre>
</div>
<p>The first line of new code creates a <code>title</code> attribute. It's a <code>QLable</code> object that will hold the window's title. Because we want to build a cool title bar, we'd like to add some custom styling to the title. To do this, we use the <code>setStyleSheet()</code> method with a string representing a CSS style sheet as an argument. The style sheet tweaks the font, borders, and margins of our title label.</p>
<p>Next, we center the title using the <code>setAlignment()</code> method with the <code>Qt.AlignmentFlag.AlignCenter</code> flag as an argument.</p>
<p>In the conditional statement, we check whether our window has a title. If that's the case, we set the text of our <code>title</code> label to the current window's title. Finally, we added the <code>title</code> label to the title bar layout.</p>
<p>The next step in our journey to build a custom title bar is to provide standard window controls. In other words, we need to add the <em>minimize</em>, <em>maximize</em>, <em>close</em>, and <em>normal</em> buttons. These buttons will allow our users to interact with our window. To create the buttons, we'll use the <code>QToolButton</code> class.</p>
<p>Here's the required code:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
# Min button
self.min_button = QToolButton(self)
min_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMinButton
)
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMaxButton
)
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarCloseButton
)
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarNormalButton
)
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
</code></pre>
</div>
<p>In this code snippet, we define all the required buttons by instantiating the <code>QToolButton</code> class. The <em>minimize</em>, <em>maximize</em>, and <em>close</em> buttons follow the same pattern. We create the button, define an icon for the buttons at hand, and set the icon using the <code>setIcon()</code> method.</p>
<p>Note that we use the standard icons that PyQt provides. For example, the <em>minimize</em> button uses the <code>SP_TitleBarMinButton</code> icon. Similarly, the <em>maximize</em> and <em>close</em> buttons use the <code>SP_TitleBarMaxButton</code> and <code>SP_TitleBarCloseButton</code> icons. We find all these icons in the <code>QStyle.StandardPixmap</code> namespace.</p>
<p>Finally, we connect the button's <code>clicked()</code> signal with the appropriate slot. For the minimize buttons, the proper slot is <code>.showMinimized()</code>. For the maximize and close buttons, the right slots are <code>.showMaximized()</code> and <code>close()</code>, respectively. All these slots are part of the main window's class.</p>
<p>The <em>normal</em> button at the end of the above code uses the <code>SP_TitleBarNormalButton</code> icon and <code>showNormal()</code> slot. This button has an extra setting. We've set its visibility to <code>False</code>, meaning that the button will be hidden by default. It'll only appear when we maximize the window to allow us to return to the normal state.</p>
<p>Now that we've created and tweaked the buttons, we must add them to our title bar. To do this, we can use the following <code>for</code> loop:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
# ...
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(28, 28))
button.setStyleSheet(
"""QToolButton { border: 2px solid white;
border-radius: 12px;
}
"""
)
title_bar_layout.addWidget(button)
</code></pre>
</div>
<p>This loop iterates over our four buttons in a predefined order. The first thing to do inside the loop is to define the focus policy of each button. We don't want these buttons to steal focus from buttons in the window's working space , therefore we set their focus policy to <code>NoFocus</code>.</p>
<p>Next, we set a fixed size of 28 by 28 pixels for the three buttons using the <code>setFixedSize()</code> method with a <code>QSize</code> object as an argument.</p>
<p>Our main goal in this section is to create a custom title bar. A handy way to customize the look and feel of PyQt widgets is to use CSS style sheets. In the above piece of code, we use the <code>setStyleSheet()</code> method to apply a custom CSS style sheet to our four buttons. The sheet defines a white and round border for each button.</p>
<p>The final line in the above code calls the <code>addWidget()</code> method to add each custom button to our title bar's layout. That's it! We're now ready to give our title bar a try. Go ahead and run the application from your command line. You'll see a window like the following:</p>
<p><img alt="A PyQt window with a custom title bar" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/window-with-custom-title-bar.png?tr=w-600 600w" loading="lazy" width="1000" height="600"/>
<em>A PyQt window with a custom title bar</em></p>
<p>This is pretty simple styling, but you get the idea. You can tweak the title bar further, depending on your needs. For example, you can change the colors and borders, customize the title's font, add other widgets to the bar, and more.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We'll apply some nicer styles later, once we have the functionality in place! Keep reading.</p>
<p>Even though the title bar looks different, it has limited functionality. For example, if you click the <em>maximize</em> button, then the window will change to its maximized state. However, the <em>normal</em> button won't show up to allow you to return the window to its previous state.</p>
<p>In addition to this, if you try to move the window around your screen, you'll quickly notice a problem: it's impossible to move the window!</p>
<p>In the following sections, we'll write the necessary code to fix these issues and make our custom title bar fully functional. To kick things off, let's start by fixing the state issues.</p>
<h2 id="updating-the-windows-state">Updating the Window's State</h2>
<p>To fix the issue related to the window's state, we'll write two new methods. We need to override one method and write another. In the <code>MainWindow</code> class, we'll override the <code>changeEvent()</code> method. The <code>changeEvent()</code> method is called directly by Qt whenever the window state changes: for example if the window is maximized or hidden. By overriding this event we can add our own custom behavior.</p>
<p>Here's the code that overrides the <code>changeEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt, QEvent
# ...
class MainWindow(QMainWindow):
# ...
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
</code></pre>
</div>
<p>This method is fairly straightforward. We check the event type to see if it is a <code>WindowStateChange</code>. If that's the case, we call the <code>window_state_changed()</code> method of our custom title bar, passing the current window's state as an argument. In the final two lines, we call the parent class's <code>changeEvent()</code> method and accept the event to signal that we've correctly processed it.</p>
<p>Here's the implementation of our <code>window_state_changed()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
</code></pre>
</div>
<p>This method takes a window's state as an argument. Depending on the value of the state parameter we will optionally show and hide the maximize & restore buttons.</p>
<p>First, if the window is currently maximized we will show the <em>normal</em> button and hide the <em>maximize</em> button. Alternatively, if the window is currently <em>not</em> maximized we will hide the <em>normal</em> button and show the <em>maximize</em> button.</p>
<p>The effect of this, together with the order we added the buttons above, is that when you maximize the window the <em>maximize</em> button will <em>appear to be</em> replaced with the <em>normal</em> button. When you restore the window to it's normal size, the <em>normal</em> button will be replaced with the <em>maximize</em> button.</p>
<p>Go ahead and run the app again. Click the <em>maximize</em> button. You'll note that when the window gets maximized, the middle button changes its icon. Now you have access to the <em>normal</em> button. If you click it, then the window will recover its previous state.</p>
<h2 id="handling-windows-moves">Handling Window's Moves</h2>
<p>Now it's time to write the code that enables us to move the window around the screen while holding your mouse's left-click button on the title bar. To fix this issue, we only need to add code to the <code>CustomTitleBar</code> class.</p>
<p>In particular, we need to override three mouse events:</p>
<ul>
<li><code>mousePressEvent()</code> will let us know when the user clicks on our custom title bar using the mouse's left-click button. This may indicate that the window movement should start.</li>
<li><code>mouseMoveEvent()</code> will let us process the window movements.</li>
<li><code>mouseReleaseEvent()</code> will let us know when the user has released the mouse's left-click button so that we can stop moving the window.</li>
</ul>
<p>Here's the code that overrides the <code>mousePressEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
</code></pre>
</div>
<p>In this method, we first check if the user clicks on the title bar using the mouse's left-click button. If that's the case, then we update our <code>initial_pos</code> attribute to the clicked point. Remember that we defined <code>initial_pos</code> and initialized it to <code>None</code> back in the <code>__init__()</code> method of <code>CustomTitleBar</code>.</p>
<p>Next, we need to override the <code>mousePressEvent()</code> method. Here's the required code:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
</code></pre>
</div>
<p>This <code>if</code> statement in <code>mouseMoveEvent()</code> checks if the <code>initial_pos</code> attribute is not <code>None</code>. If this condition is true, then the <code>if</code> code block executes because we have a valid initial position.</p>
<p>The first line in the <code>if</code> code block calculates the difference, or<code>delta</code>, between the current and initial mouse positions. To get the current position, we call the <code>position()</code> method on the <code>event</code> object and convert that position into a <code>QPoint</code> object using the <code>toPoint()</code> method.</p>
<p>The following four lines update the position of our application's main window by adding the <code>delta</code> values to the current window position. The <code>move()</code> method does the hard work of moving the window.</p>
<p>In summary, this code updates the window position based on the movement of our mouse. It tracks the initial position of the mouse, calculates the difference between the initial position and the current position, and applies that difference to the window's position.</p>
<p>Finally, we can complete the <code>mouseReleaseEvent()</code> method:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class CustomTitleBar(QWidget):
# ...
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
</code></pre>
</div>
<p>This method's implementation is pretty straightforward. Its purpose is to reset the initial position by setting it back to <code>None</code> when the mouse is released, indicating that the drag is complete.</p>
<p>That's it! Go ahead and run your app again. Click on your custom title bar and move the window around while holding the mouse's left-click button. Can you move the window? Great! Your custom title bar is now fully functional.</p>
<p>The completed code for the custom title bar is shown below.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QSize, Qt, QEvent
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.ColorRole.Highlight)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setStyleSheet(
"""font-weight: bold;
border: 2px solid black;
border-radius: 12px;
margin: 2px;
"""
)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
# Min button
self.min_button = QToolButton(self)
min_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMinButton
)
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarMaxButton
)
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarCloseButton
)
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = self.style().standardIcon(
QStyle.StandardPixmap.SP_TitleBarNormalButton
)
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# Add buttons
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(28, 28))
button.setStyleSheet(
"""QToolButton { border: 2px solid white;
border-radius: 12px;
}
"""
)
title_bar_layout.addWidget(button)
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
central_widget = QWidget()
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
def window_state_changed(self, state):
self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<h2 id="making-it-a-little-more-beautiful">Making it a little more beautiful</h2>
<p>So far we've covered the technical aspects of styling our window with a custom title bar, and added the code to make it function as expected. But it doesn't look <em>great</em>. In this section we'll take our existing code & tweak the styling and buttons to produce something that's a little more professional looking.</p>
<p>One common reason for wanting to apply custom title bars to a window is to integrate the title bar with the rest of the application. This technique is called a <em>unified title bar</em> and can be seen in some popular applications such as web browsers, or Spotify.</p>
<p><img alt="Unified title bar in Spotify" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/unified-titlebar.png?tr=w-600 600w" loading="lazy" width="534" height="334"/></p>
<p>In this section we'll look at how we can reproduce the same effect in PyQt using a combination of stylesheets & icons. Below is a screenshot of the final result which we'll be building.</p>
<p><img alt="Style custom title bar in PyQt6" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/styled-window-custom-title-bar.png?tr=w-600 600w" loading="lazy" width="1018" height="588"/></p>
<p>As you can see the window & the toolbar blend nicely together and the window has rounded corners. There are a few different ways to do this, but we'll cover a simple approach using Qt stylesheets to apply styling over the entire window.</p>
<p>In order to customize the shape of the window, we need to first tell the OS to stop drawing the default window outline and background for us. We do that by setting a <em>window attribute</em> on the window. This is similar to the flags we already discussed, in that it turns on & off different window manager behaviors.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python"># ...
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# ...
</code></pre>
</div>
<p>We've added a call to <code>self.setAttribute</code> which sets the attribute <code>Qt.WidgetAttribute.WA_TranslucentBackground</code> on the window. If you run the code now you will see the window has become transparent, with only the widget text & toolbar visible.</p>
<p>Next we'll tell Qt to draw a new <em>custom</em> background for us. If you've worked with QSS before, the most obvious way to apply curved edges to the window using QSS stylesheets would be to set <code>border-radius:</code> styles on the main window directly, e.g.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">#...
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# ...
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setStyleSheet("background-color: gray; border-radius: 10px;")
#...
</code></pre>
</div>
<p>However, if you try this you'll notice that it doesn't work. If you enable a translucent background, the background of the window is not drawn (including your styles). If you <em>don't</em> set translucent background, the window is filled to the edges with a solid color ignoring the border radius.</p>
<p><img alt="Stylesheets can't alter window shape" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/stylesheet-window-styling.jpg?tr=w-600 600w" loading="lazy" width="1850" height="608"/>.</p>
<p>The good news is that, with a bit of lateral thinking, there is a simple solution. We already know that we can construct interfaces by nesting widgets in layouts. Since we can't style the <code>border-radius</code> of a window, but we <em>can</em> style any other widget, the solution is to simply add a container widget into our window & apply the curved-edge and background styles to that.</p>
<p>On our <code>MainWindow</code> object we already have a <em>central widget</em> which contains our layout, so we can apply the styles there.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# ...
central_widget = QWidget()
# This container holds the window contents, so we can style it.
central_widget.setObjectName("Container")
central_widget.setStyleSheet("""#Container {
background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
border-radius: 5px;
}""")
self.title_bar = CustomTitleBar(self)
# ...
</code></pre>
</div>
<p>We've taken the existing <code>central_widget</code> object and assigned an <em>object name</em> to it. This is a ID which we can use to refer to the widget from QSS, to apply our styles specifically to that widget.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> If you're familiar with CSS you might expect that IDs like <code>#Container</code> must be unique. However, they are not: you can give multiple widgets the same object name if you like. So you can re-use this technique and QSS on multiple windows in your application without problems.</p>
<p>With this style applied on our window, we have a nice gradient background with curved corners.</p>
<p>Unfortunately, the title bar we created is drawn filled, and so the background and curved corners of our window are over-written. To make things look coherent we need to make our title bar also transparent by removing the background color & auto-fill behavior we set earlier.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> We don't need to set any flags or attributes this widget because it is not a window. A <code>QWidget</code> object is transparent by default.</p>
<p>We can also make some tweaks to the style of the title label, such as adjusting the font size and making the title capitalized using <code>text-transform: uppercase</code> -- feel free to customize this yourself.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# self.setAutoFillBackground(True) # <-- remove
# self.setBackgroundRole(QPalette.ColorRole.Highlight) # <-- remove
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setStyleSheet("""
QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
""")
</code></pre>
</div>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> QSS is <em>very</em> similar to CSS, especially for text styling.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The <code>margin-left: 48px</code> is to compensate for the 3 * 16px window icons on the right hand side so the text align centrally.</p>
<p>The icons are currently using built-in Qt icons which are a little bit plain & ugly. Next let's update the icons, using custom SVG icons of simple colored circles, for the minimize, maximize, close & restore buttons.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtGui import QIcon
# ...
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# ...
# Min button
self.min_button = QToolButton(self)
min_icon = QIcon()
min_icon.addFile('min.svg')
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = QIcon()
max_icon.addFile('max.svg')
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = QIcon()
close_icon.addFile('close.svg') # Close has only a single state.
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = QIcon()
normal_icon.addFile('normal.svg')
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# ...
</code></pre>
</div>
<p>This code follows the same basic structure as before, but instead of using the built-in icons here we're loading our icons from SVG images. These images are very simple, consisting of a single circle in green, red or yellow for the different states mimicking macOS.</p>
<p class="admonition admonition-note"><span class="admonition-kind"><i class="fas fa-sticky-note"></i></span> The <code>normal.svg</code> file for returning a maximized window to normal size shows a semi-transparent green circle for simplicity's sake, but you can include iconography and hover behaviors on the buttons if you prefer.</p>
<p class="admonition admonition-tip"><span class="admonition-kind"><i class="fas fa-lightbulb"></i></span> You can download these icons & all source code for this tutorial here: https://downloads.pythonguis.com/custom-title-bar-pyqt6.zip</p>
<p>The final step is to iterate through the created buttons, adding them to title bar layout. This is slightly tweaked from before to remove the border styling replacing it with simple padding & setting the icon sizes to 16px. Because we are using SVG files the icons will automatically scale to the available space.</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
# ...
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(16, 16))
button.setStyleSheet(
"""QToolButton {
border: none;
padding: 2px;
}
"""
)
title_bar_layout.addWidget(button)
</code></pre>
</div>
<p>And that's it! With these changes, you can now run your application and you'll see a nice sleek modern-looking UI with unified title bar and custom controls.</p>
<p><img alt="Style custom titlebar in PyQt6" src="https://www.pythonguis.com/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg" srcset="https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-100 100w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-200 200w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-400 400w, https://ik.imagekit.io/mfitzp/pythonguis/static/tutorials/advanced-ui-features/custom-title-bar/pyqt6-custom-modern-title-bar.jpg?tr=w-600 600w" loading="lazy" width="1748" height="1052"/>
<em>The final result, showing our unified title bar and window design.</em></p>
<p>The complete code is shown below:</p>
<div class="code-block">
<span class="code-block-language code-block-python">python</span>
<pre><code class="python">from PyQt6.QtCore import QEvent, QSize, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QMainWindow,
QStyle,
QToolButton,
QVBoxLayout,
QWidget,
)
class CustomTitleBar(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.initial_pos = None
title_bar_layout = QHBoxLayout(self)
title_bar_layout.setContentsMargins(1, 1, 1, 1)
title_bar_layout.setSpacing(2)
self.title = QLabel(f"{self.__class__.__name__}", self)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setStyleSheet(
"""
QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
"""
)
if title := parent.windowTitle():
self.title.setText(title)
title_bar_layout.addWidget(self.title)
# Min button
self.min_button = QToolButton(self)
min_icon = QIcon()
min_icon.addFile("min.svg")
self.min_button.setIcon(min_icon)
self.min_button.clicked.connect(self.window().showMinimized)
# Max button
self.max_button = QToolButton(self)
max_icon = QIcon()
max_icon.addFile("max.svg")
self.max_button.setIcon(max_icon)
self.max_button.clicked.connect(self.window().showMaximized)
# Close button
self.close_button = QToolButton(self)
close_icon = QIcon()
close_icon.addFile("close.svg") # Close has only a single state.
self.close_button.setIcon(close_icon)
self.close_button.clicked.connect(self.window().close)
# Normal button
self.normal_button = QToolButton(self)
normal_icon = QIcon()
normal_icon.addFile("normal.svg")
self.normal_button.setIcon(normal_icon)
self.normal_button.clicked.connect(self.window().showNormal)
self.normal_button.setVisible(False)
# Add buttons
buttons = [
self.min_button,
self.normal_button,
self.max_button,
self.close_button,
]
for button in buttons:
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
button.setFixedSize(QSize(16, 16))
button.setStyleSheet(
"""QToolButton {
border: none;
padding: 2px;
}
"""
)
title_bar_layout.addWidget(button)
def window_state_changed(self, state):
if state == Qt.WindowState.WindowMaximized:
self.normal_button.setVisible(True)
self.max_button.setVisible(False)
else:
self.normal_button.setVisible(False)
self.max_button.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom Title Bar")
self.resize(400, 200)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
central_widget = QWidget()
# This container holds the window contents, so we can style it.
central_widget.setObjectName("Container")
central_widget.setStyleSheet(
"""#Container {
background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
border-radius: 5px;
}"""
)
self.title_bar = CustomTitleBar(self)
work_space_layout = QVBoxLayout()
work_space_layout.setContentsMargins(11, 11, 11, 11)
work_space_layout.addWidget(QLabel("Hello, World!", self))
centra_widget_layout = QVBoxLayout()
centra_widget_layout.setContentsMargins(0, 0, 0, 0)
centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
centra_widget_layout.addWidget(self.title_bar)
centra_widget_layout.addLayout(work_space_layout)
central_widget.setLayout(centra_widget_layout)
self.setCentralWidget(central_widget)
def changeEvent(self, event):
if event.type() == QEvent.Type.WindowStateChange:
self.title_bar.window_state_changed(self.windowState())
super().changeEvent(event)
event.accept()
def window_state_changed(self, state):
self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.initial_pos = event.position().toPoint()
super().mousePressEvent(event)
event.accept()
def mouseMoveEvent(self, event):
if self.initial_pos is not None:
delta = event.position().toPoint() - self.initial_pos
self.window().move(
self.window().x() + delta.x(),
self.window().y() + delta.y(),
)
super().mouseMoveEvent(event)
event.accept()
def mouseReleaseEvent(self, event):
self.initial_pos = None
super().mouseReleaseEvent(event)
event.accept()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
</code></pre>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, we have learned the fundamentals of creating custom title bars in PyQt. To do this, we have combined PyQt's widgets, layouts, and styling capabilities to create a visually appealing title bar for a PyQt app.</p>
<p>With this skill under your belt, you're now ready to create title bars that align perfectly with your application's unique style and branding. This will allow you to break away from the standard window decoration provided by your operating system and add a personal touch to your user interface.</p>
<p>Now let your imagination run and transform your PyQt application's UX.</p>