Reputation: 373
The documentation for PyQt6 states that
Support for Qt’s resource system has been removed (i.e. there is no
pyrcc6
).
In light of this, how should one provide resources for a PyQt6 application?
Upvotes: 9
Views: 13967
Reputation: 2478
Inspired by ekhumoro's answer and K.Mulier's answer, I'd provide my code to automatically convert the .ui and .qrc files, just put the following code to a file named ui2pyqt6usingPySide6.py
in a folder that shares the same parent dir as your .ui and .qrc files, then all you need to do is to run it.
Note that you should ensure PySide6(pip install -U PySide6
) installed before using it.
import os
import sys
import subprocess
import sys
# >>> sys.base_exec_prefix
# 'C:\\Users\\Oscar\\AppData\\Local\\Programs\\Python\\Python37-32'
# >>> sys.base_prefix
# 'C:\\Users\\Oscar\\AppData\\Local\\Programs\\Python\\Python37-32'
# >>> sys.exec_prefix
# 'C:\\Users\\Oscar\\AppData\\Local\\Programs\\Python\\Python37-32'
# >>> sys.prefix
# 'C:\\Users\\Oscar\\AppData\\Local\\Programs\\Python\\Python37-32'
# >>>
# pyqt
# uicPath = os.path.join(sys.base_exec_prefix, "Scripts/pyuic6.exe")
# uicPath = os.path.join(sys.base_exec_prefix, "Scripts/pyuic5.exe")
# rccPath = os.path.join(sys.base_exec_prefix, "Scripts/pyrcc5.exe")
# print(uicPath)
# pyside
uicPath = os.path.join(sys.base_exec_prefix, "Scripts/pyside6-uic.exe")
rccPath = os.path.join(sys.base_exec_prefix, "Scripts/pyside6-rcc.exe")
def convert(processArgs):
# ENCODING = 'utf8'
try:
output = subprocess.check_output(processArgs) # ,用了 universal_newlines=True 本地解析会出解码问题"--debug" http://www.iqiyi.com/ 出现异常
except Exception as e:
print('no ', processArgs)
else:
print('yes ', processArgs)
# return str(output, ENCODING) # output.decode(ENCODING) #
with open(outputFilePath, "r+") as f:
data = f.read()
f.seek(0)## Move the cursor to the beginning of the file
f.write(data.replace('PySide6', 'PyQt6'))
f.truncate()# Remove any remaining content after the modified part
with os.scandir(os.curdir) as it:
for entry in it:
if entry.name.endswith('.ui'):
# print(entry.name, entry.path)
outputFilePath = os.path.join(os.path.dirname(entry.path), 'Ui_{}.py'.format(entry.name[:-3]))
convert([uicPath, entry.path, '-o', outputFilePath])
elif entry.name.endswith('.qrc'):
# print(entry.name)
outputFilePath = os.path.join(os.path.dirname(entry.path), '{}_rc.py'.format(entry.name[:-4]))
convert([rccPath, entry.path, '-o', outputFilePath])
# ================================================================================================ #
# ENUM CONVERTER TOOL
#https://stackoverflow.com/a/72658216/1485853
# ================================================================================================ #
from typing import *
import os, argparse, inspect, re
q = "'"
help_text = '''
Copyright (c) 2022 Kristof Mulier
MIT licensed, see bottom
ENUM CONVERTER TOOL
===================
The script starts from the toplevel directory (assuming that you put this file in that directory)
and crawls through all the files and folders. In each file, it searches for old-style enums to
convert them into fully qualified names.
HOW TO USE
==========
Fill in the path to your PyQt6 installation folder. See line 57:
pyqt6_folderpath = 'C:/Python39/Lib/site-packages/PyQt6'
Place this script in the toplevel directory of your project. Open a terminal, navigate to the
directory and invoke this script:
$ python enum_converter_tool.py
WARNING
=======
This script modifies the files in your project! Make sure to backup your project before you put this
file inside. Also, you might first want to do a dry run:
$ python enum_converter_tool.py --dry_run
FEATURES
========
You can invoke this script in the following ways:
$ python enum_converter_tool.py No parameters. The script simply goes through
all the files and makes the replacements.
$ python enum_converter_tool.py --dry_run Dry run mode. The script won't do any replace-
ments, but prints out what it could replace.
$ python enum_converter_tool.py --show Print the dictionary this script creates to
convert the old-style enums into new-style.
$ python enum_converter_tool.py --help Show this help info
'''
# IMPORTANT: Point at the folder where PyQt6 stub files are located. This folder will be examined to
# fill the 'enum_dict'.
# pyqt6_folderpath = 'C:/Python39/Lib/site-packages/PyQt6'
# EDIT: @Myridium suggested another way to fill this 'pyqt6_folderpath'
# variable:
import PyQt6
pyqt6_folderpath = PyQt6.__path__[0]
# Figure out where the toplevel directory is located. We assume that this converter tool is located
# in that directory. An os.walk() operation starts from this toplevel directory to find and process
# all files.
toplevel_directory = os.path.realpath(
os.path.dirname(
os.path.realpath(
inspect.getfile(
inspect.currentframe()
)
)
)
).replace('\\', '/')
# Figure out the name of this script. It will be used later on to exclude oneself from the replace-
# ments.
script_name = os.path.realpath(
inspect.getfile(inspect.currentframe())
).replace('\\', '/').split('/')[-1]
# Create the dictionary that will be filled with enums
enum_dict:Dict[str, str] = {}
def fill_enum_dict(filepath:str) -> None:
'''
Parse the given stub file to extract the enums and flags. Each one is inside a class, possibly a
nested one. For example:
---------------------------------------------------------------------
| class Qt(PyQt6.sip.simplewrapper): |
| class HighDpiScaleFactorRoundingPolicy(enum.Enum): |
| Round = ... # type: Qt.HighDpiScaleFactorRoundingPolicy |
---------------------------------------------------------------------
The enum 'Round' is from class 'HighDpiScaleFactorRoundingPolicy' which is in turn from class
'Qt'. The old reference style would then be:
> Qt.Round
The new style (fully qualified name) would be:
> Qt.HighDpiScaleFactorRoundingPolicy.Round
The aim of this function is to fill the 'enum_dict' with an entry like:
enum_dict = {
'Qt.Round' : 'Qt.HighDpiScaleFactorRoundingPolicy.Round'
}
'''
content:str = ''
with open(filepath, 'r', encoding='utf-8', newline='\n', errors='replace') as f:
content = f.read()
p = re.compile(r'(\w+)\s+=\s+\.\.\.\s+#\s*type:\s*([\w.]+)')
for m in p.finditer(content):
# Observe the enum's name, eg. 'Round'
enum_name = m.group(1)
# Figure out in which classes it is
class_list = m.group(2).split('.')
# If it belongs to just one class (no nesting), there is no point in continuing
if len(class_list) == 1:
continue
# Extract the old and new enum's name
old_enum = f'{class_list[0]}.{enum_name}'
new_enum = ''
for class_name in class_list:
new_enum += f'{class_name}.'
continue
new_enum += enum_name
# Add them to the 'enum_dict'
enum_dict[old_enum] = new_enum
continue
return
def show_help() -> None:
'''
Print help info and quit.
'''
print(help_text)
return
def convert_enums_in_file(filepath:str, dry_run:bool) -> None:
'''
Convert the enums in the given file.
'''
filename:str = filepath.split('/')[-1]
# Ignore the file in some cases
if any(filename == fname for fname in (script_name, )):
return
# Read the content
content:str = ''
with open(filepath, 'r', encoding='utf-8', newline='\n', errors='replace') as f:
content = f.read()
# Loop over all the keys in the 'enum_dict'. Perform a replacement in the 'content' for each of
# them.
for k, v in enum_dict.items():
if k not in content:
continue
# Compile a regex pattern that only looks for the old enum (represented by the key of the
# 'enum_dict') if it is surrounded by bounds. What we want to avoid is a situation like
# this:
# k = 'Qt.Window'
# k found in 'qt.Qt.WindowType.Window'
# In the situation above, k is found in 'qt.Qt.WindowType.Window' such that a replacement
# will take place there, messing up the code! By surrounding k with bounds in the regex pat-
# tern, this won't happen.
p = re.compile(fr'\b{k}\b')
# Substitute all occurences of k (key) in 'content' with v (value). The 'subn()' method re-
# turns a tuple (new_string, number_of_subs_made).
new_content, n = p.subn(v, content)
if n == 0:
assert new_content == content
continue
assert new_content != content
print(f'{q}{filename}{q}: Replace {q}{k}{q} => {q}{v}{q} ({n})')
content = new_content
continue
if dry_run:
return
with open(filepath, 'w', encoding='utf-8', newline='\n', errors='replace') as f:
f.write(content)
return
def convert_all(dry_run:bool) -> None:
'''
Search and replace all enums.
'''
for root, dirs, files in os.walk(toplevel_directory):
for f in files:
if not f.endswith('.py'):
continue
filepath = os.path.join(root, f).replace('\\', '/')
convert_enums_in_file(filepath, dry_run)
continue
continue
return
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description = 'Convert enums to fully-qualified names',
add_help = False,
)
parser.add_argument('-h', '--help' , action='store_true')
parser.add_argument('-d', '--dry_run' , action='store_true')
parser.add_argument('-s', '--show' , action='store_true')
args = parser.parse_args()
if args.help:
show_help()
else:
#& Check if 'pyqt6_folderpath' exists
if not os.path.exists(pyqt6_folderpath):
print(
f'\nERROR:\n'
f'Folder {q}{pyqt6_folderpath}{q} could not be found. Make sure that variable '
f'{q}pyqt6_folderpath{q} from line 57 points to the PyQt6 installation folder.\n'
)
else:
#& Fill the 'enum_dict'
type_hint_files = [
os.path.join(pyqt6_folderpath, _filename)
for _filename in os.listdir(pyqt6_folderpath)
if _filename.endswith('.pyi')
]
for _filepath in type_hint_files:
fill_enum_dict(_filepath)
continue
#& Perform requested action
if args.show:
import pprint
pprint.pprint(enum_dict)
elif args.dry_run:
print('\nDRY RUN\n')
convert_all(dry_run=True)
else:
convert_all(dry_run=False)
print('\nQuit enum converter tool\n')
Upvotes: 1
Reputation: 156
For anyone here who is migrating from pyqt5:
The migration from pyqt5 to pyside6 turned out to be easier for me than going from pyqt5 to pyqt6 since we get pyside6-rcc
and pyside6-uic
Easy way to add Apple Silicon/M1/M2 support to a legacy app
Upvotes: 0
Reputation: 7889
Someone decided pyrcc6 wasn't useful and no longer provide it. However, what they don't understand is how useful it is for those of us that use qt designer to define our resources like icons.. etc and then using pyinstaller to package and find all resources, so that pyinstaller can build a stand-alone exe and these icons are properly embedded and used in the .ui user interface.
You may find this link useful (I had no success with it though): https://pypi.org/project/pyqt6rc/
Ultimately, I switched from pyqt5 to pyside6 using pip which includes pyside6-rcc command to do the same thing that the old pyrcc5 command used to do. BTW pyqt6 has way too many enumeration and that makes transistion to pyqt6 painful, which is another good reason to change to pyside6.
The best full explanation can be found at this YouTube link:
https://www.youtube.com/watch?v=u5BLPTkbaM8
Upvotes: 2
Reputation: 141
I liked the answer by musicamante, but I could not apply it directly since I use Qt Designer and pyuic6 to generate the python code for the UI. I did however find a workaround. It builds on using a resource file in Qt Designer, since I already did that with the previous PyQt versions.
<RCC>
<qresource prefix="/">
<file>icons/myicon.png</file>
</qresource>
</RCC>
sed 's/:\/icons\//icons:/g' pyuic6_output_file.py > patched_file.py
QtCore.QDir.setSearchPaths("icons", [os.path.join(os.path.dirname(__file__), 'icons')])
Upvotes: 1
Reputation: 338
Adding further to youtube video (Update 28/12/22) https://www.youtube.com/watch?v=u5BLPTkbaM8
Do it with the simple script:
pyuic6 tip.ui > tip.py && sed -i '10iimport _cf_rc\nimport _rc_rc' tip.py
Explaination here:
sed -i "10i' -> means insert from the 10th line onwards.
I have 2 files to insert in tip.py:
import _cf_rc
import _rc_rc
Upvotes: 0
Reputation: 120638
UPDATE:
As of PyQt-6.3.1, it's possible to use Qt’s resource system again. (This version now includes the qRegisterResourceData
and qUnregisterResourceData
functions which are required by the generated python resource module.)
There's still no pyrcc6 tool, but Qt's own rcc tool can now be used to convert the qrc file. This tool should be installed by default with a full Qt6 installation, but if you can't find it, you could also use the PySide6 tools to convert the qrc file. (PySide6 simply searches for the Qt6 rcc tool and runs it using subprocess
, so it will produce exactly the same output).
Thus, to convert the qrc file, you can now use either:
rcc -g python -o resources.py resources.qrc
or:
pyside6-rcc -o resources.py resources.qrc
However, it's very important to note that the import line at the top of the generated file must be modified to work correctly with PyQt6:
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost!
# from PySide6 import QtCore <-- replace this line
from PyQt6 import QtCore
The whole operation can be done with this unix one-liner (requires GNU sed):
rcc -g python resources.qrc | sed '0,/PySide6/s//PyQt6/' > resources.py
or:
pyside6-rcc reources.qrc | sed '0,/PySide6/s//PyQt6/' > resources.py
Once this small change has been made, the generated module can be safely imported into the main application, like this:
from PyQt6 import QtCore, QtGui, QtWidgets
from test_ui import Ui_Window
import resources
class Window(QtWidgets.QWidget, Ui_Window):
def __init__(self):
super().__init__()
self.setupUi(self)
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.show()
app.exec()
Note that it is NOT SAFE to use the generated module without making the changes noted above. This is because the unmodfied module will attempt to import PySide6, which is obviously inappropriate for a PyQt6 application. Whilst it may seem to work on the development machine, there's no guarantee that mixing the two libararies in this way will always remain compatible - and in any case, it's bad practice to enforce the installation of PySide6 on a user's system just so that they can run a PyQt6 application.
OLD ANSWER:
The consensus seems to be that the existing python facilities should be used instead of pyrrc. So the resources would be stored directly in the file-system (perhaps within archive files), and then located using importlib.resources (python >= 3.7), or pkg_resources, or a third-party solution like importlib_resources. Exactly how this maps to existing uses of pyrcc will probably be application-specific, so some experimentation will be needed to find the best approach.
For more details on how to use these facilities, see:
Upvotes: 19
Reputation: 43
for those people who want a real and simple solution just watch it here: link
a guy figured it out by converting the "resource.qrc" into a .py file by using the pyrcc of PySide6. then importing the resource.py (same as before) in your PyQt6 project. everything is the same, including the special filepath syntax: ":/image.jpg" instead of "./image.jpg"
hope it helps, always feels good to have a simpler solution.
Upvotes: 1
Reputation: 2550
As I started to use PyQt6, I found missing full support for Qt6 Resources. Especially when using designer and using images for buttons, labels etc. I tried addSearchPath, but still had to edit generated .py template. After some research I found using importlab the best solution for my problem.
I made simple script, which is using .qrc file and generates .py templates with importpath.
For example changing:
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/icons/icon1.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
to:
icon = QtGui.QIcon()
with path("myPackage.resources.icons", "icon1.png") as f_path:
icon.addPixmap(QtGui.QPixmap(str(f_path)), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
Here is a link to GitLab repo: https://github.com/domarm-comat/pyqt6rc
Or install via pip:
python3 -m pip install pyqt6rc
Upvotes: 1
Reputation: 48345
There has been some discussion on the PyQt mailing list when this was found out.
The maintainer is not interested in maintaining pyrcc anymore as he believes that it doesn't provide any major benefit considering that python already uses multiple files anyway.
The easiest solution is probably to use the static methods of QDir setSearchPaths()
or addSearchPath()
.
The difference will be that resources will be loaded using the prefix used for the methods above.
Considering the previous situation:
icon = QtGui.QIcon(':/icons/myicon.png')
Now it would become like this:
# somewhere at the beginning of your program
QtCore.QDir.addSearchPath('icons', 'path_to_icons/')
icon = QtGui.QIcon('icons:myicon.png')
Upvotes: 11