GUI with gauges in python - Page 1
Forum section: electronics / general

author: Simon
author_rank: Unknown
timestamp: November 10, 2024, 03:29:56 pm
content: So time has come that I have a little some here and there to play.I'm looking at Python for better or worse to create an application on a single board computer to run on a small screen. I have found that TKinter is the default graphics thingy and OK. It is of course old fashioned. and there are no dial or gauge type of widget. I see people have created their own libraries for TKinter to make these. Is this a sensible way to go or am I just starting on the wrong foot?I have read very little about tkinter so I don't know how easy it is to create my own stuff with it.

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 10, 2024, 04:30:21 pm
content: Quote from: Simon on November 10, 2024, 03:29:56 pmBeen there, done that. Tkinter is fast running, but can be low level, and a lot of work if you have to work around not having a mouse or an equivalent. The other issue is that it uses callbacks, but you can hide them all with a little thought (it took me about a day's worth).

author: Simon
author_rank: Unknown
timestamp: November 10, 2024, 04:32:50 pm
content: why is a callback an issue?

author: globoy
author_rank: Frequent Contributor
timestamp: November 10, 2024, 05:03:56 pm
content: I have only done the equivalent of a "hello world" with it, but LVGL has a micropython binding. LVGL has gauges, and a lot of other nice UI capabilities including anti-aliasing.https://github.com/lvgl/lv_binding_micropythonI know you're looking for a python solution and there have been discussions about how to make it run there too but I'm not sure where they are currently.https://forum.lvgl.io/t/use-lvgl-with-python3/10686

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 10, 2024, 05:19:28 pm
content: Quote from: Simon on November 10, 2024, 04:32:50 pmA callback isn't, nested callbacks, once deep enough, are.

author: mariush
author_rank: Super Contributor
timestamp: November 10, 2024, 05:44:23 pm
content: Dear Imgui would probably work for you :https://github.com/ocornut/imguiSeehttps://pyimgui.readthedocs.io/en/latest/guide/first-steps.html

author: kripton2035
author_rank: Super Contributor
timestamp: November 10, 2024, 06:19:07 pm
content: is there any IDE where you can define windows and buttons and menus and all with a mouse and not with dozens of lines of text ?

author: Simon
author_rank: Unknown
timestamp: November 10, 2024, 06:21:30 pm
content: Quote from: mariush on November 10, 2024, 05:44:23 pmProbably? so what can it do that TKinter can't?

author: Simon
author_rank: Unknown
timestamp: November 10, 2024, 06:24:50 pm
content: Quote from: globoy on November 10, 2024, 05:03:56 pmI would love to use lvgl, that is what the current protype is done in but it was done on an ESP32 (because someone had a solution looking for a problem). I want to redo it on linux. I suppose that as python is simply a bunch of wrappers for all that is already out there I may as well look at C++ again and using LVGL that way.

author: ejeffrey
author_rank: Super Contributor
timestamp: November 10, 2024, 11:23:17 pm
content: Quote from: kripton2035 on November 10, 2024, 06:19:07 pmThere are lots of tool that will let you lay out a bunch of widgets graphically. You of course still have to write the event handlers and update routines. I have used QT designer (I think it goes by a different name now, it's been ages). Basically you would drag and drop the widgets into place, and then each would have a properties dialog where you could select how events mapped to functions in your code. There are similar design tools for other graphical toolkits, but that's the only reasonably mainstream one I have used. Getting it to look "professional" and especially to get it to resize well is still a fair bit of work. Also, you can't easily incorporate dynamically generated / enabled UI elements in a straightforward way. But it's definitely a tool that can get a simple but functional UI up and running in as little as an hour or so.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 11, 2024, 03:36:42 am
content: Quote from: kripton2035 on November 10, 2024, 06:19:07 pmFor Qt, you use Qt Designer (forQt 5orQt 6). You can run Qt on any OS and desktop environment, including Windows, as long as you also have Qt installed.For GTK+, you useGlade. Don't worry, you're not required to use GNOME desktop with GTK+; any desktop environment will work, as long as you have GTK+ installed.There are two options: Either you let the editor generate the code for you, or you save the UI description as an XML file (.ui, for both cases, although the syntax is different). I recommend the latter, so that you can keep modifying it afterwards without any changes to your code. It is slightly more complicated to implement at first, but reduces maintenance cost/complexity.You can find a visual list of Qt 5 widgetshere, Qt 6 widgetshere, GTK+3 widgetshere, and GTK+4 widgetshere.Both also support graphics area widgets, which are basically canvases. You write adrawfunction, that redraws the entire widget from scratch whenever called. Qt/GTK+ will call it whenever necessary. For continuous dial updates, you can use animation features provided by the toolkit, and/or add a timeout that triggers a redraw. (Basically, with the animation support, the toolkit will tell you the time elapsed since the last redraw, if you need that for e.g. decay effects when update frequency varies.) They have a complete set of drawing functions, including all sorts of transformations you might need.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 11, 2024, 05:34:24 am
content: Apologies for the wall of text, but if you want to do Python UIs in Linux, especially for microcontrollers or network widgets, you might want to read this.Quote from: Simon on November 10, 2024, 03:29:56 pmI'd use Qt5. If you can draw an image of what kind of widgets you want, I can write a Python 3 example for you.I like to use Inkscape for these (but note that you'll need to upload the resulting SVG file somewhere else than here; I use my own webhost), with the followingDocument Propertiesset in the following order:Format:mmWidth:display width in mmHeight:display height in mmViewbox:X: 0Y: 0Width:display width in pixelsHeight:display height in pixelsScale: (autocalculated as the pixel pitch in mm)You can set theDisplay unitsto whatever units you prefer to work with. I also often set up one or moreRectangular gridin the Grids tab in Document Properties, for easier alignment of things. The#key will show/hide the grid. The%key controls the global snapping to grid, but you have finer control using the ▸ icon next to the magnet at top right corner of the Inkscape window. Advanced mode lets you select exactly what snaps to grid or guides; I use it heavily.This way, whatever you design, you can immediately print and look at it as if it was the physical display, exact correct size and all. If your display settings are correct, opening the SVG image in Inkscape at 100% zoom, or your browser, should show it in correct size as well. As it is a vector image format, the fidelity is optimal at all scales.(Also makes an excellent medium for mockups for remote clients. If one also has the time to learn HTML + CSS + Javascript, the SVG images can be converted to HTML pages –– like myhome page is, it is a single self-contained 21,749-byte HTML file with no other files or dependencies ––, andmade interactive–– like myFIR spectrum page; just stuff the filter coefficients like1 0 -2 0 1for a bandpass filter or1 1for a low-pass filter in the top right rectangle, and it will draw the filter spectral response for you, also in a single self-contained HTML file that works in a browser even locally without any internet connection.)You can choose either PySide2 or PyQt5 bindings for Python 3. If you usefrom PyQt5 import QtCore, QtGui, QtWidgetsfrom PyQt5.uic import loadUiorfrom PySide2 import QtCore, QtGui, QtWidgets, QtUiToolsdef loadUi(uifile):return QtUiTools.QUiLoader().load(uifile)they will be identical for practical purposes.loadUiis used to load XML.uifiles, including those used in Qt Designer. If you do not use.uifiles, thenfromtoolkitimport QtCore, QtGui, QtWidgetssuffices.As I love to use SVG files, I also addQtSvgto the import list.The namespacing is such thatQtWidgetsandQtGuiclasses and constants willalwaysneed the correspondingQtWidgets.orQtGui.prefix, andQt coreconstants and such aQtCore.prefix. This can be a bit annoying at first, because you will forget, and get lots of errors, but remembering it is in most cases this namespacing issue, will help. Some constants are specific to that class, so you'll need to prefix the entire class name. For example, theQGraphicsScenewidget (for drawing your own dials and complex graphics to) mentionsQPenandQBrush; you need to remember that here, you need to refer to them asQtGui.QPenandQtGui.QBrushinstead.If you wanted to display say nice RPM and speed gauges, you can use a JPEG or PNG image (or SVG image, if you useQtSvg) as the background for your own widgets.For providing the dial/gauge inputs, I like to use USB serial. Ineveruse Qt USB or Serial abstractions, or libusb variants, because they just do not perform sufficiently well in my opinion. For Python, I use the built-intermioslibrary for access –– only works in Linux, Android, macOS, BSDs; not in Windows. The serial device name is either fixed or passed as a command-line parameter, and in Linux an udev rule is needed to allow access and to set up a freely-nameable symlink. It's just a line or two of text and easy to set up.The serial communication will be run in a separate thread (two, if you use bidirectional communications with the USB device), usingQtCore.QThread. Whenever a thread receives a new status change, for example new potentiometer state, it uses the Qt signal mechanism to trigger an update. Whenever the UI wants to cause something to happen, maybe control a LED, it uses the Qt signal mechanism to trigger the sender thread to send the command. This also means that for interactive buttons, like one that stays "pressed" while the LED is on, you have a signal slot for LED became on/off (or separate slots, if you prefer), and when the user clicks on the button, only the signal is sent (and the visual state of the button is not changed), and only when the signal is received on the slot, is the visual button status updated.This way, the UI will reflect the external device status, and not "lie" to the user. I think an user interface where clicking a button appears to work immediately, but there is a human-detectable interval between that and the physical event. I'd rather have the interface reflect the physical situation, with the button press/release visually showingwhenthe thing has occurred.Also, the serial communications is then not limited to query-response, but can have multiple concurrent queries in flight at the same time. It does mean each query should have an identifier (an increasing number, like Gcode line numbers), which is included in the response. It does not matter to me at all whether the communications are text or binary; Python built-instructlibrary makes it trivial to adjust.If you happen to haveTeensy 4.0orTeensy 4.1microcontrollers, then making an experimental/fake version of the USB inputs is trivial in the Arduino environment. You simply selectSerialfromTools>USB Typemenu, and useSerial.write()to send the data. If you call that for arrays/blocks of data of at least 16 bytes or so, you can get 25 Mbytes/sec (200+ Mbits/s) data rate, in one direction. If you have soldered pins on it, stick it on a breadboard, and connect some 10k linear potentiometers and LEDs with 1k resistors (1.3mA at Vf=2.0V), and with just a dozen or two lines, you're go.Network communications, like a dashboard for network gadgets, is very similar, using Python built-insocketlibrary. (It is available even in Windows; it is only not available in WebAssembly (WASI) environments). For IP/UDP -based protocols, a single thread can handle both incoming and outgoing traffic. For TCP/IP, I like to use separate threads if possible, because certain TCP operations are blocking at the kernel level, and unless signal delivery based timeout mechanism is used, can lead to DDOS vulnerabilities if connecting to nefarious peers/services. If you trust the other ends, then a single thread suffices, even for multiple connections.)Because Python is easy to interface to native dynamic libraries via thectypeslibrary, I recommend using it to implement any proprietary stuff. It is also excellent for implementing TCP/IP services to many clients, with the "actions" implemented in Python. The API to the library will be visible, but does not need to be licensed the same as the Python code. In particular, if you use LGPL'd Qt modules (say, QtCore, QtWidgets, QtGui, QtSvg), you can use any license for the Python code, and any license for the native dynamic libraries – which do not need to be open sourced at all. In my opinion, this approach gets the best of all worlds: the users can tweak the UI if they ever need to, developer gets to keep the Secret Sauce proprietary, and Qt is used according to the licenses. If you also refer to Qt use (via LGPL) and promote it in the documentation, both Qt Project and the Qt Group (formerly Qt Company) will be happy with you.It is possible to compile a version of Qt to use EGLFS Qt Platform Abstraction (qeglfs for Qt 5,for Qt 6), which lets a single application to run fullscreen with OpenGL ES 2.0 acceleration on bare Linux, without X/Wayland et cetera. This is licensed under a number of different licenses, including LGPL. Technically, this is calledQt for Embedded Linux. Note the Linux in there. The main work to do this is in building all the parts for your target architecture correctly; plus to keep everyone happy, if you distribute the code/binaries, providing the exact sources you used is necessary. I personally would also describe step-by-step how to rebuild the libraries in the documentation, both to keep developers happy, but also as a safety net against overeager lawyers. (With that information and showing they can be used to rebuild identical binaries, any lawsuit wrt. LGPL that is about Qt and not LGPL, would get summary judgment in most jurisdictions. But neither current Qt developers or Qt Group/Company would bring such a lawsuit in the first place.)Qt Embeddedis related, but a larger set of mostly proprietary Qt modules for which you need a license from the Qt Group. You can see the basic frame license agreement terms and conditionshere. It includes a lot of useful stuff beyond what I described above, but is not required for any of the stuff I've described above. So, for small developers and prototype design is likely not that useful, only becoming useful when your sales and development cost warrant the added usefulness.

author: Smokey
author_rank: Super Contributor
timestamp: November 11, 2024, 06:48:00 am
content: For simple, single window, and internal use GUIs with python I use tkinter. It's built in and low resource, which is nice.This is a cool trick. Using actual language, describe in as much detail as you can exactly what you want your GUI to look like and where you want all the interactive elements. Also give names to anything you want named specific things. Dump that all into ChatGPT as a prompt and it will do all the boiler plate tkinter for you.The new Claude is pretty good at this too. It will even render the thing and show you what the GUI will look like if you ask it.While I've used QT with python before for a more involved application, I'm pretty sure I won't do that again.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 11, 2024, 10:50:29 am
content: Here is a tiny dialog example I wrote forthis threadin March 2024, slightly edited.First, you create an.uifile, sayexample.ui, attached asexample.ui.txt. You can open it in Qt Creator, and modify it as you like.For ease of use, name your interactive widgets so that in the Python code can automatically implement handlers for the corresponding events (signals in Qt parlance) by naming the handleron_widgetName_signal.With a fixed-size display, make it a fullscreen window of the correct size, and you don't need to worry about the intricacies of scaling. This example does not set the scaling properties, so if you resize the window, the widgets will stay in its upper left corner. For proper scaling (so widgets will expand to the available space), you need to use a geometry manager, some subclass ofQLayout; I typically useQFormLayoutfor stuff like this dialog, andQGridLayoutfor everything else.The corresponding Python code for the example dialog isCode:#!/usr/bin/env python3# -*- coding: utf-8 -*-# SPDX-License-Identifier: CC0-1.0## This is in public domain. Written by Nominal Animal, 2024.#import systry:# Prefer PyQt5,from PyQt5 import QtCore, QtGui, QtWidgetsfrom PyQt5.uic import loadUiexcept ImportError as err:# but fall back on PySide2.from PySide2 import QtCore, QtGui, QtWidgets, QtUiToolsdef loadUi(uifile):return QtUiTools.QUiLoader().load(uifile)class ExampleDialog(object):def __init__(self):super().__init__()# Load UI,self.ui = loadUi("example.ui")# and connect on_<widget>_<signal> to corresponding members as handlers:for slot in dir(self):if slot.startswith("on_") and slot.find("_", 3) > 3:method = getattr(self, slot)widgetName, signalName = slot[3:].rsplit("_", 1)source = self.ui.findChild(QtCore.QObject, widgetName)signal = getattr(source, signalName, None)if signal:signal.connect(lambda *args, **kwargs: method(source, *args, **kwargs))# Display this ui.self.show = self.ui.show# Example handlerdef on_radioButton_3_USB_clicked(self, widget, clicked):print("clicked on %s" % widget.objectName())if __name__ == '__main__':app = QtWidgets.QApplication(sys.argv)# Set these. Among other things, QtCore.QSettings(scope) uses these.app.setOrganizationName('The Name of Your Organization')app.setApplicationName('The Name of This Application')win = ExampleDialog()win.show()status = app.exec_()if win.ui.result() == QtWidgets.QDialog.Accepted:if win.ui.findChild(QtWidgets.QRadioButton, "radioButton_1_LAN").isChecked():print("LAN: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP").text())elif win.ui.findChild(QtWidgets.QRadioButton, "radioButton_2_COM").isChecked():print("COM: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP_2").text())elif win.ui.findChild(QtWidgets.QRadioButton, "radioButton_3_USB").isChecked():print("USB: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP_3").text())else:print("None selected")else:print("Canceled")del windel appsys.exit(status)The firsttry..exceptclause uses PyQt5 bindings if available, and falls back to PySide2 otherwise, in a way that makes the two practically identical to our Python code. If you wanted to control the bindings using environment variable, that part expands by about 20 lines, where an environment variable, typicallyQT_API. Instead of this, you can useQtPyinstead, but I dislike the added abstraction and maintenance load of yet another dependency.Like all widget toolkits, but unlikeSDL, your program releases all control to the widget toolkit, and then the toolkit will trigger events and call your code. You can implement an idle handler, which will be called whenever all pending events have been processed, and you can implement timeouts that cause an event to be generated in the future, for animations and such.In Qt, for graphical UI stuff, you first instantiate aQtWidgets.QApplicationclass. (Code that does not use any QtWidgets can useQtGui.QGuiApplication, and code that runs in batch mode without any graphics can useQtCore.QCoreApplication.)This object then represents the application itself, and owns all windows you create. There are some nifty tricks there for multidocument handling, but most importantly, you use theapp.postEvent()to send events to your widgets from other threads. You can also useapp.setStyle()to set the application look and feel (seeQtWidgets.QStyle).(If you use threads, do not subclass subclassQtCore.QThread, but use the plain QtCore.QThread. In its.run()method, instantiate your own subclass ofQtCore.QObjectfor the communication with other threads. This way, Qt will handle the signal passing between threads correctly, with each thread owning the Qt widget or object running the event code.)Qt provides a way to get and set settings objects in both system (machine) and user scopes, viasystemSettings =QtCore.QSettings(QtCore.QSettings.SystemScope)userSettings =QtCore.QSettings(QtCore.QSettings.UserScope)To set a value, use the.setValue(name,value)method;nameis always a string, butvaluecan be of any type. Conversely,.value(name)or.value(name,default)method to access them. These are automatically loadedand savedfor you. You can also use the.sync()method to save the contents, but I don't bother. Note that your constructors (__init__()methods) and other functions can access the settings this same way. To find out if there have been any errors, check the.status()method. If everything is okay, it should be equal toQtCore.QSettings.NoError.Next, you instantiate your windows. They do not need to become instantly visible, as they by default stay hidden until you call.show()on them or their parents. In Python, the__init__()method is the constructor, called whenever the class is instantiated. Thesuper().__init__()is the Python 3 way of calling the__init__()method of the parent type first, so you almost always have it first in your subclass constructors. If there are parameters that can be passed to the constructor, you can use formclassmyClassName(extendsClass):def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)whereargsis the list of positional parameters (and*argsexpands that list in place for function parameters, much like varargs in C), andkwargsis a dictionary of named parameters (and**kwargsexpands that for function parameters).Because I load the entire graphical user interface from an.uifile, my main window class is not actually an Qt object, but a plain Python one. Its.uimember will have the widget hierarchy. I prefer this approach, because this way I will have no collisions with the Qt instance or class methods and properties.When the.exec_()method of the application object is called, control passes to Qt. This call returns only when the application is closing. The return value is usually 0, but if one usesapplication.exit(value)or similar methods, that value is returned.In this dialog example, the main program then calls the dialog window.result()function, to see whether the selection was accepted or canceled. (Closing the window is the same as canceling.)Finally, I make it easier for the Python garbage controller by deleting the main window object first, then the application object. If you passed a reference to the application object to your window hierarchy, I recommend you set it toNonebefore deleting the main window object. This ensures that the various destructors get called, and in the correct order.sys.exit(value)passes the status code, zero being No Errors (but all others application-specific and between 1 and 127), to the operating system.If you want to create custom widgets, like say a speedometer like gauge, and you want to use it in the.uifile, things become a bit messier since you'd need to provide the implementation to Qt Designer also.I don't usually do that. Instead, I use the base class in Qt Designer, basically setting it in the layout, and then create a copy of the.uifile with the class name replaced, usingsed -e 's| class="QClass"| class="myclass"|g'original.ui>runtime.uiI then also use ONLYPySide2bindings, since that makes the extension easier, viaCode:def loadUi(uifile, custom=None, parent=None):from PySide2 import QtUiToolsloader = QtUiTools.QUiLoader()if custom is not None:for cls in custom:loader.registerCustomWidget(cls)ui = loader.load(uifile, parent)del loaderdel QtUiToolsreturn uiwhere you supply a list of your custom classes as the second parameter toloadUi().Note that the Qt.uifile syntax is based on XML, and is more or less human-readable.So, I wouldn't say this is a lot of work. A bit complicated, maybe, because of the many details, but I've listed the key points above.In Linux, you can install bothPySide2andPyQt5bindings at the same time. I do findPySide2a bit more complete, considering the.uifile loading mechanisms, so I guess I should prefer that.For massive data processing, I recommend using Python built-inmultiprocessingmodule, as the current Python interpreter can only execute code in one thread at a time per process. Using parallel processes allows your Python code to use more than one CPU core concurrently. Even better solution is to write it in C or some other compiled code into a dynamic library; just make the Python-facing API thread-safe, and use threads (pthreadsin C).One particular benefit of using Qt is that its graphics primitives are pretty well optimized to work on things like OpenGL ES; even moreso if you useQtWidgets.QOpenGLWidget. SeeQtGui.QOpenGLFunctionsfor what is available (via.context().functions()).For example, if you wanted an oscilloscope-type display, then having a list ofQtCore.QPointvalues defining the samples, each list will be rendered by hardware in a single call. Similarly for blitting textures. Remember that PNG images have transparency, and OpenGL and OpenGL ES supports textures with transparency. A speedometer dial you could render by having three images: the background image, an image of the dial arrow shadow in center position (horizontal or vertical), and an image of the dial arrow in center position. When rendering, the background would be copied, then the rotated shadow (with alpha channel) drawn, and finally the rotated arrow (with alpha channel) drawn. With a bit of preparation, the three images would be stored in an OpenGL buffer, so the entire thing would basically be rendered in hardware. Even though you're using Python.

author: Psi
author_rank: Super Contributor
timestamp: November 11, 2024, 10:59:02 am
content: Quote from: jfiresto on November 10, 2024, 05:19:28 pmAnd best not to even think about nested interrupts containing nested callbacks.

author: djsb
author_rank: Super Contributor
timestamp: November 11, 2024, 12:06:26 pm
content: This guy likes his gaugeshttps://www.youtube.com/@VolosProjects/videos

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 12, 2024, 07:35:56 pm
content: Thanks to Nominal - I haven't written any GUIs with Python so far, but his example works fine (just tested) and this will be a handy start if I need to.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 13, 2024, 12:41:06 am
content: I'm also completely serious here. I'd love to do a full example, say as a new thread here, but I just don't know what use case other people would find most interesting. It's a crucial choice, because it defines how useful my efforts would be to others! The web is already full of examples that don't really fit practical needs at all.I could very easily make an example where you use any microcontroller with a couple of pots and LEDs, maybe a button, and an example interactive Python GUI talking to it via serial.I have some 128×32 and 128×64 OLEDs, so another I could do is an old-style Pixel Editor (remember sprite editors of C64 era?). Or maybe font tester?Even a custom serial console is doable, it's just that full VT220/ANSI escape code parsing takes quite a few lines (because each command needs their own handler, and even minimal support needs at least a couple of dozen) and I'd need help verifying they are all implemented correctly.A command/response interface would be much easier, even if you used a binary protocol. It could even be an alternative UI for the pots-and-LEDs microcontroller case.