When I use QFontMetrics in PyQt5, the numbers I get don't match the size of the text that I'm drawing with drawText - pyqt5

I'm trying to get the size of a text, so I can scale it accordingly to fit inside a box. But unfortunately the QFontMetrics.width() seems to give wrong outputs.
Here's a code that draws a text, and uses values from QFontMetrics to draw a rect that should be similar size. But it's not. As you can see in the picture below, the values from QFontMetrics (drawn rect) are about half of the one that I'm drawing. And unfortunately I can't just multiply it by 2, because depending on the text, the factor might be 1.85 or 1.95.
from PyQt5 import QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtGui import QPainter, QTextDocument, QFont, QFontMetrics
from PyQt5.QtCore import QRect, Qt, QRectF
import sys
font = QFont("times",10)
fm = QFontMetrics(font)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setFont(font)
sText = 'Hello World!'
painter.drawText(0,100, sText)
pixelsWide = fm.width(sText)
pixelsHigh = fm.height()
painter.drawRect(0, 100, pixelsWide, pixelsHigh)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

As explained in the QFont documentation:
Note that a QGuiApplication instance must exist before a QFont can be used.
This obviously including usage of the QFont as a QFontMetrics constructor.
The reason is simply logic and quite obvious: the QApplication must be aware of the UI environment in order to properly compute font metrics, which might depend on the paint device they're going to be drawn upon. Consider the common case of font scaling or High DPI settings: without a QGuiApplication, Qt has absolutely no meanings of knowing those aspects, and QFont shouldn't (nor could) obviously take care of that in its constructor, as QFontMetrics wouldn't.
Move the QFont and QFontMetrics constructor somewhere else, which could be in any moment after the QApplication creation and before their actual usage.

Related

How to apply CSS color filter on Folium map tiles

I want to create a dark mode based on Google Maps tiles in Folium. However, as Google is not provided dark mode tiles, a simple workaround seems to be applying a color filter to tiles. A similar plugin for Leaflet is introduced here.
How can I reach a similar result in Folium? Is it possible by executing javascript through the runJavaScript() method (similar to what was done here)?
A minimal Foilium map embedded in PyQt5 is also provided.
import io
import folium
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QMainWindow
from PyQt5.QtWebEngineWidgets import QWebEngineView
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.m = folium.Map(
zoom_start = 18,
location = (41.8828, 12.4761),
control_scale=True,
tiles = None
)
folium.raster_layers.TileLayer(
tiles='http://mt1.google.com/vt/lyrs=m&h1=p1Z&x={x}&y={y}&z={z}',
name='Standard Roadmap',
attr = 'Google Map',
).add_to(self.m)
folium.LayerControl().add_to(self.m)
self.data = io.BytesIO()
self.m.save(self.data, close_file=False)
widget=QWidget()
vbox = QVBoxLayout()
self.webView = QWebEngineView()
self.webView.setHtml(self.data.getvalue().decode())
self.webView.setContextMenuPolicy(Qt.NoContextMenu)
vbox.addWidget(self.webView)
widget.setLayout(vbox)
self.setCentralWidget(widget)
self.setWindowTitle("App")
self.setMinimumSize(1000, 600)
self.showMaximized()
App = QApplication([])
window = Window()
App.exec()

PyQt5 rotation pixmap

In Pyqt5 I want to rotate a pixmap but every time i tried it changes the size.
My code is:
import math
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
from PyQt5.QtCore import QObject, QPointF, Qt, QRectF,QRect
from PyQt5.QtGui import QPixmap, QTransform, QPainter
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__()
self.arch1 = QPixmap("arch1.png")
pm = QPixmap(556,556)
rectF = QRectF(0,0,556,556)
painter = QPainter(pm)
painter.drawPixmap(rectF, self.arch1, rectF)
painter.end()
self.label = QLabel("AAAAAAAAAA")
self.label.setPixmap(pm)
butA = QPushButton("A")
butA.clicked.connect(lambda: self.rotate_item())
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(butA)
self.setLayout(layout)
self.show()
def rotate_item(self):
rectF = QRectF(0,0,556,556)
self.arch1 = self.arch1.transformed(QTransform().rotate(36))
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.drawPixmap(rectF, self.arch1,QRectF(self.arch1.rect()))
painter.end()
self.label.setPixmap(pix)
def main():
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
if __name__=="__main__":
main()
I want only rotate not resize. What do you suggest me to do?
I have four other files and i want to rotate differently. i post some photos to understand what i want to do.
any other way to do this?
Circle one
Circle two
Complete circle
The problem is that the rotated pixmap has a bigger bounding rectangle.
Consider the following example:
The light blue square shows the actual bounding rectangle of the rotated image, which is bigger.
Using drawPixmap(rectF, self.arch1, QRectF(self.arch1.rect())) will cause to painter to draw the pixmap in the rectangle rectF, using the new bounding rectangle as source, so it becomes "smaller".
Instead of rotating the image, you should rotate the painter. Since transformations by default use the origin point of the painter (0, 0), we need first to translate to the center of the rectangle, rotate the painter, and then retranslate back to the origin.
Note that in the following example I'm also always drawing starting from the source image, without modifying it: continuously applying a transformation will cause drawing artifacts due to aliasing, and after some rotation the quality would be very compromised.
The rotation variable is to keep track of the current rotation.
class Window(QWidget):
def __init__(self, *args, **kwargs):
# ...
self.rotation = 0
def rotate_item(self):
self.rotation = (self.rotation + 36) % 360
rectF = QRectF(0,0,556,556)
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.translate(rectF.center())
painter.rotate(self.rotation)
painter.translate(-rectF.center())
painter.drawPixmap(0, 0, self.arch1)
painter.end()
self.label.setPixmap(pix)

pyqt5 QLabel Image setScaledContents(True) don't allow Qpainter updates

I want to display an image and put a marker at the current mouse position for every left mouse click.
Below code does the job however, it works only if ("self.imglabel.setScaledContents(True)") is commented. Any reason?
I have to do this job on various images of different resolutions, I read to maintain the proper aspect ratio and display the image appropriately we need to use setScaledContents(True). But why enabling this is not allowing update() (PaintEvent)??
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QSizePolicy, QMessageBox
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QImage, QPalette
class Menu(QMainWindow):
def __init__(self):
super().__init__()
self.central_widget = QWidget() # define central widget
self.setCentralWidget(self.central_widget)
self.vbox = QVBoxLayout(self.central_widget)
self.vbox.addWidget(self.imgWidget())
self.vbox.addWidget(QPushButton("test"))
def imgWidget(self):
self.imglabel = QLabel()
self.imglabel.setScaledContents(True)
self.image = QImage("calib.jpeg")
self.imagepix = QPixmap.fromImage(self.image)
self.imglabel.setPixmap(self.imagepix)
self.imglabel.mousePressEvent = self.imgMousePress
return self.imglabel
def imgMousePress(self, e):
painter = QPainter(self.imglabel.pixmap())
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
painter.drawPoint(e.x(), e.y())
painter.end()
self.imglabel.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainMenu = Menu()
mainMenu.show()
sys.exit(app.exec_())
To avoid unnecessary computation for each paintEvent of the QLabel, whenever the scaledContents property is True the scaled image is cached, and all the painting is automatically discarded.
To avoid that, you should create a new instance of QPixmap using the existing one, and then set the new painted pixmap again. Note that if the image is scaled, the widget coordinates won't reflect the actual position on the pixmap, so you need to use a transformation to get the actual point to paint at.
def imgMousePress(self, e):
pm = QPixmap(self.imglabel.pixmap())
painter = QPainter(pm)
pen = QPen()
pen.setWidth(10)
pen.setColor(QColor('red'))
painter.setPen(pen)
transform = QTransform().scale(
pm.width() / self.imglabel.width(),
pm.height() / self.imglabel.height())
painter.drawPoint(transform.map(e.pos()))
painter.end()
self.imglabel.setPixmap(pm)
Consider that all the "points" will become stretched rectangles if the width/height ratio is not the same of the source image, but this is only a problem of appearance: if you save the pixmap later, they will be squares again, since saving is based on the source pixmap.
If you want to keep their squared shape while displaying instead, you'll need to keep track of the points and overwrite paintEvent to paint them manually on the label.

matplotlib animation embedded in tkinter : update function never called

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method
As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

pyplot example with QT Designer

I have seen many examples of integrating matplotlib with python2.6 and QT4 Designer using mplwidget and they work fine. I now need to integrate a pyplot with QT4 Designer but cannot find any examples. All of my attempts to integrate a pyplot graphic have ended in a seg fault. Can someone please direct me to a working example using Designer and pyplot?
Follow up:
Okay, so I tried your solution but I'm still having issues. Unfortunately the machine I use for this code is not hooked up to the internet, so below is a fat finger of the pertinent parts of the code I am using:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import MatplotlibWidget # This is the code snippet you gave in your answer
.
.
.
def drawMap(self) :
fig = plt.figure()
map = Basemap(projection='cyl', llcrnrlat = -90.0, urcrnrlat = 90.0, llcrnrlon = -180.0, urcrnrlon = 180.0, resolution = 'c')
map.drawcoastlines()
map.drawcountries()
plt.show()
def drawMap_Qt(self) :
self.ui.PlotWidget.figure = plt.figure() # This is the Qt widget promoted to MatplotlibWidget
map = Basemap(projection='cyl', llcrnrlat = -90.0, urcrnrlat = 90.0, llcrnrlon = -180.0, urcrnrlon = 180.0, resolution = 'c')
map.drawcoastlines()
map.drawcountries()
self.ui.PlotWidget.show()
The function drawMap() works fine. It creates a separate window and plots the map. The drawMap_Qt() function results in a segmentation fault with no other errors. The end goal is to plot a contour on top of the map. I can do this with the drawMap() function. Of course, I can't even get to the contour part with the drawMap_Qt() function. Any insights as to why it is seg faulting would be greatly appreciated.
If you're referring to the mplwidget from Python(x,y) and WinPython, I think it does use pyplot, but I had trouble putting it in my python install so I just used this class:
from PyQt4 import QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class MatplotlibWidget(QtGui.QWidget):
def __init__(self, parent=None, *args, **kwargs):
super(MatplotlibWidget, self).__init__(parent)
self.figure = Figure(*args, **kwargs)
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
self.setLayout(layout)
See also How to embed matplotib in pyqt - for Dummies.
Then in Qt Designer you need to create a promoted class, with base class: QWidget, promoted class name: MatplotlibWidget, and header file: the python script containing the MatplotlibWidget class (without the .py). You can add things like ax = self.figure.add_subplot(111), line = ax.plt(...) within the class or by calling methods of the figure attribute of an instance of the class.
Edit:
So I was a bit wrong before, in general with embedded matplotlib widgets you need to use the object oriented methods and not the functions in pyplot. (This sort of explains what the difference is.) Using my snippet above as mymatplotlibwidget.py, try something like this. I don't have basemap installed, so this is somewhat of a guess, but from the examples you need to tell Basemap which axes to use.
import sys
from PyQt4 import QtGui
from mpl_toolkits.basemap import Basemap
from mymatplotlibwidget import MatplotlibWidget
app = QtGui.QApplication(sys.argv)
widget = MatplotlibWidget()
fig = widget.figure
ax = fig.add_subplot(111)
map = Basemap(..., ax=ax)
fig.canvas.draw()
widget.show()
app.exec_()