【日常随笔】万字长文,如何用pyside6开发一个python桌面工具

本文主要内容搬运自本人CSDN以下博文,欢迎大家关注!

前言

在研发测试日常工作中,通常会遇到很多琐碎的事情,占用我们工作的时间和精力,从而导致我们不能把大部分的注意力放在主要的工作上面。为了解决这个问题,除了加人之外,我们通常会开发一些日常用的效率工具,比如以pyqt、pyside为主体的桌面应用,一键化我们的日常工作,从而解放我们很多处理琐碎事情的精力,让我们有更多精力打磨主业,创造更好的工作成绩。

因此,本文就分享下笔者在24年下半年调研学习pyside6的一些成果,把自己做的小应用homemade-toolset开源出来,并且把技术实现的过程也呈现出来,供各位有需要的同学参考学习,如何用pyside6开发一个python桌面工具。

整个项目包含时钟工具、JSON工具以及类似Postman的HTTP请求工具,采用python3.11和pyside6开发。接下来看一下,整个项目具体怎么搭建起来。

初始化项目

初始化项目并不难,先强调一点是,所有的资料都可以在官网查到。如果有特别疑问的地方,参考官网,实在不行就stackoverflow或者gpt,也许可以更快解决问题。

首先是折腾项目工作区。从个人开发角度,笔者推荐所有的桌面开发项目都放在一个pyside6的工作区,并采用venv来安装pyside6相关库和工具。

pyside6的工具有很多,比如把ui文件转化为python代码的pyside6-uic,以及编辑ui的可视化工具pyside6-designer之类。如果是venv安pyside6的话,这些工具都集成到了${project_dir}/.venv/bin下面,有需要的话也可以export到path里,具体作用详细可以参考官网的这份资料。通过这些工具加上一些脚本,就能简单打通ui编辑->ui转码->代码编写->部署发布的开发链路(p.s. 部署发布相关的调研暂时不多)。

代码组织方面,推荐先是把工具类、业务逻辑和ui逻辑几个模块分离开,然后重要一点是,把ui生成代码和实际的window跟widget类给分开来,做到view和model的区分。这样一来是大小层次比较分明,不会出现循环引用的情况,二来是从ui生成的代码,也不会直接影响到已有代码的实现,做改动也是非常方便。在homemade-toolset项目中,最终呈现的代码结构是:

  • app:应用内容
    • init.py:app初始化逻辑
    • service:业务逻辑
    • util:工具类
    • view:前端逻辑
      • component:可复用的ui组件
      • ui:通过pyside6-designer生成的ui代码
      • worker:后台异步/并发的任务类
      • XXX.py:主页面逻辑
  • cfg:配置文件
  • etc:静态资源
    • ui:pyside6-designer的ui文件
    • script:研发脚本
      • deploy.sh:app打包,workdir为项目根目录
      • uic.py:pyside6-designer的ui文件转py文件的脚本,workdir为项目根目录
  • main.py:程序入口
  • pysidedeploy.spec
  • README.md:项目介绍

以这样的项目结构,通过app文件夹就可以存储所有业务逻辑,最外层的main.py就能够驱动app运行:

python 复制代码
import app


if __name__ == '__main__':
    app.run()

然后在app的__init__.py启动整个项目

python 复制代码
APP: Optional[QtWidgets.QApplication] = None


def run():
    global APP
    APP = QtWidgets.QApplication([])

    window = MainWindow()
    window.ensure_center()
    window.show()

    sys.exit(APP.exec())

最后在每个ui类实现里面来初始化跟定义界面逻辑:

python 复制代码
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self._init_actions()
        self._init_widget()

    def _init_actions(self):
        self.ui.actionAbout.triggered.connect(self.show_about)
        self.ui.actionExit.triggered.connect(self.close)

        self.ui.actionSupport.triggered.connect(self.show_support)

    def _init_widget(self):
        # 主动set中心widget,后续可以通过配置化方式灵活设置不同的界面
        self.setCentralWidget(ToolWidget())

    def closeEvent(self, event):  # 关闭窗口时触发
        reply = QMessageBox.question(
            self, '确认', '是否要退出程序?',
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.Yes)
        if reply == QMessageBox.StandardButton.Yes:
            event.accept()
        else:
            event.ignore()

界面设计

界面设计上,用一个TabWidget就可以简单做个门户入口,区分多个子功能的界面,每个子功能也可以单独在一个ui界面去设计。

每一个Widget里面需要定义组件的排版(layout),在designer里面,一般是Widget包含了一个组件才能够编辑layout。layout有很多种,垂直(vertical)的话类似于web前端里面一个个Row的排列,水平(horizontal)的话则类似于一个个Col的排列,栅格(Grid)布局的话每个组件的占用面积可能和旁边组件相关联,还有一种表单(Form)布局则专门用于填写表单配置类,是label+input的阻塞。

在layout限制下,每个组件可能占据一块区域,但组件自己也有一些填充策略。像PushButton,一般水平或垂直策略需要调整成fixed,保证不填充到整个区域,要按照自己本身的大小来填,而比如Spacer,需要做占位把组件分割成左右两块的,默认会设置成Expanding来占位。

layout的作用,比方说,做一个json格式转换工具,可以先用一个HorizontalLayout设置两行,第一行工具栏,第二行是TextEdit输入输出界面。工具栏设置成VerticalLayout,装几个Button外加Spacer填充空位,输入输出可以直接占第二行的左右两边,也是搞成VerticalLayout。右边的TextEdit弄成只读,左边的TextEdit用于输入yaml、json等字符串,这样再结合自己实现的model跟service逻辑,整个模块就差不多做起来了。

时钟工具

时钟工具前端主要展现当前时间的绘制以及一系列timestamp、datetime和timestring的转换,整块难点主要在时钟的绘制上,没有一手资料,但经过一些源码研究还是可以解决的。由于pyside6本身有QLCDNumber控件的支持,所以绘制起来比较容易,官网也给了一个例子。笔者自己则在这个基础上做了下修改,代码如下:

python 复制代码
import sys

from PySide6.QtCore import QTime, QTimer, Slot
from PySide6.QtWidgets import QApplication, QLCDNumber


class DigitalClock(QLCDNumber):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSegmentStyle(QLCDNumber.Flat) # 设置显示样式,可以看哪个比较美观
        self.setDigitCount(8)

        # 定时器每秒更新
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.show_time)
        self.timer.start(1000)

        self.show_time()

    @Slot()
    def show_time(self):
        time = QTime.currentTime()
        text = time.toString("hh:mm:ss") # 直接格式化显示即可

        # Blinking effect,视情况使用
        # if (time.second() % 2) == 0:
        #     text = text.replace(":", " ")

        self.display(text)


if __name__ == "__main__": # 测试代码
    app = QApplication(sys.argv)
    clock = DigitalClock()
    clock.show()
    sys.exit(app.exec())

而表盘时钟则相对难一些,主要是需要拿到一个比较好的效果。经过一番寻找,在这个例子里面展示的效果比较不错。笔者也是在这个基础上做了一点调整,详细代码如下:

python 复制代码
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import QPoint, QTimer, QTime, Qt
from PySide6.QtGui import QGuiApplication, QPainter, QPalette, QPolygon

class AnalogClock(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(AnalogClock, self).__init__(parent)

        # 每1s更新绘图
        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect(self.update)
        self._timer.start(1000)

        # 绘制时针、分针、秒针,粗细形状可以调整QPoint参数
        self._hour_hand = QPolygon([
            QPoint(4, 14),
            QPoint(-4, 14),
            QPoint(-3, -55),
            QPoint(3, -55)
        ])
        self._minute_hand = QPolygon([
            QPoint(4, 14),
            QPoint(-4, 14),
            QPoint(-3, -85),
            QPoint(3, -85)
        ])
        self._seconds_hand = QPolygon([
            QPoint(1, 14),
            QPoint(-1, 14),
            QPoint(-1, -90),
            QPoint(1, -90)
        ])

        # 调色板,调整时针、分针、秒针和背景色
        palette = qApp.palette()
        self._background_color = palette.color(QPalette.ColorRole.Base)
        self._hour_color = palette.color(QPalette.ColorRole.Text)
        self._minute_color = palette.color(QPalette.ColorRole.Text)
        self._seconds_color = palette.color(QPalette.ColorRole.Accent)

    def paintEvent(self, event):  # 重载paintEvent函数来画图
        width = self.width()
        height = self.height()
        side = min(width, height)

        with QPainter(self) as painter:  # painter有前后处理,with一下
            # 绘制背景
            painter.fillRect(0, 0, width, height, self._background_color)
            painter.setRenderHint(QPainter.Antialiasing)
            painter.translate(width / 2, height / 2)
            painter.scale(side / 200.0, side / 200.0)

            time = QTime.currentTime()

            # 通过setBrush设置绘图颜色,绘制时针
            painter.setPen(Qt.NoPen)
            painter.setBrush(self._hour_color)
            painter.save()
            painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0)))
            painter.drawConvexPolygon(self._hour_hand)
            painter.restore()

            # 绘制小时的12点
            for _ in range(0, 12):
                painter.drawRect(73, -3, 16, 6)
                painter.rotate(30.0)

            # 绘制分针
            painter.setBrush(self._minute_color)
            painter.save()
            painter.rotate(6.0 * time.minute())
            painter.drawConvexPolygon(self._minute_hand)
            painter.restore()

            # 绘制秒针,带一个针头
            painter.setBrush(self._seconds_color)
            painter.save()
            painter.rotate(6.0 * time.second())
            painter.drawConvexPolygon(self._seconds_hand)
            painter.drawEllipse(-3, -3, 6, 6)
            painter.drawEllipse(-5, -68, 10, 10)
            painter.restore()

            # 绘制分钟的60分
            painter.setPen(self._minute_color)
            for _ in range(0, 60):
                painter.drawLine(92, 0, 96, 0)
                painter.rotate(6.0)

HTTP请求工具

JSON工具的实现比较简单,这里按下不表,直接跳到重头戏,HTTP请求工具。要做一个类似postman的HTTP请求工具,首先界面设计方面,直接抄postman的一些基本元素就可以了,比如说:

  • Request:Method、URL、SubmitButton、Headers、Body、Settings
  • Response:Headers、Body、StatusCode、ElapsedTime

每个组件可能还有比较细微的需求,比如Headers可以做成QTableWidget的形式,支持增删改功能,用setRowCount、setItem、removeRow之类的接口就可以满足这些操作。Settings可以简单做成FormLayout,设置超时时间就可以。ElapsedTime则在ui设计上需要留空内容,请求的时候动态渲染当前请求消耗了多少秒钟。

ui设计差不多好了之后,我们需要写单独的一个http请求模块,让http请求的执行和审计可以独立出来运行。这是因为,桌面工具ui渲染是大头,不是说执行了一个http请求ui渲染就会阻塞,这样界面就会卡住了。怎么样不让界面卡住这是另一个话题,先解决http请求,让其可以单独执行。

我们可以基于requests库来实现http调用逻辑,需要单独把Request和Response类抽象出来。直接上代码:

python 复制代码
import requests


class Response:
    def __init__(self,
                 status_code: int = 200,
                 headers: Optional[Dict[str, str]] = None,
                 body: str = None):
        # 只需要状态码、headers、body
        self.status_code = status_code
        self.headers = headers if isinstance(headers, dict) else {}
        self.body = body

    @classmethod
    def from_response(cls, resp: requests.Response):  # 从requests的返回中提取内容
        status_code = resp.status_code
        headers = dict(resp.headers)
        body = resp.text
        return cls(status_code, headers, body)


class RequestMethod:
    GET = 'GET'
    POST = 'POST'
    PUT = 'PUT'
    DELETE = 'DELETE'
    PATCH = 'PATCH'


def _default_request_headers():
    return {
        'Content-Type': 'application/json',
    }


class Request:
    def __init__(self,
                 url: str = '',
                 method: str = RequestMethod.GET,
                 headers: Optional[Dict[str, str]] = None,
                 body: str = '',
                 settings: Optional[RequestSettings] = None):
        # 对应界面设置里头的东西
        self.url = url
        self.method = method
        self.headers = headers if isinstance(headers, dict) else _default_request_headers()
        self.body = body
        self.settings = settings if isinstance(settings, RequestSettings) else RequestSettings()

    def validate(self):
        if not self.url:
            return ValueError('url is required')
        if not self.method:
            return ValueError('method is required')
        return None

    def args(self):  # 组成requests的参数
        return {
            'method': self.method,
            'url': self.url,
            'headers': self.headers,
            'data': self.body.strip(),
            'timeout': (
                self.settings.connect_timeout_seconds(),
                self.settings.read_timeout_seconds()
            )
        }

    def invoke(self) -> (Response, Exception):
        try:
            resp = requests.request(**self.args())
            return Response.from_response(resp), None
        except Exception as e:
            return None, e

有了这些代码之后,单个http请求就可以独立运行,并且请求、返回的数据上下文也可以单独审计。

接下来要解决的问题是,怎么把view逻辑和这个request串联起来。这里需要用到QThread加上signal的机制,通过一个RequestWorker串联,保证http请求干扰不到ui运转。

View层面,逻辑可以简写成这样:

python 复制代码
class ToolWidget(QWidget):
    def __init__(self):
        super(ToolWidget, self).__init__()
        self.ui = Ui_ToolWidget()
        self.ui.setupUi(self)

        # request worker
        self._request_worker: Optional[RequestWorkerThread] = None

        # init actions and widget
        self._init_actions()
        self._init_widget()

    def _init_actions(self):
        # do request
        self.ui.requestInvokeButton.clicked.connect(self.invoke_request)
        
        # headers CRUD
        self.ui.requestHeadersResetButton.clicked.connect(self.reset_request_headers)
        self.ui.requestHeadersAddButton.clicked.connect(self.add_request_header)
        self.ui.requestHeadersRemoveButton.clicked.connect(self.remove_request_header)

    def _init_widget(self):
        # fill in request
        default_request = Request()
        self.ui.requestMethodComboBox.setCurrentText(default_request.method)

        # request headers
        self.reset_request_headers()

        # request settings
        default_request_settings = default_request.settings
        self.ui.requestSettingsConnectTimeoutLineEdit.setText(str(default_request_settings.connect_timeout))
        self.ui.requestSettingsReadTimeoutLineEdit.setText(str(default_request_settings.read_timeout))

    def _reset_request_state(self):  # 重置request界面和worker
        """clear all previous request states"""
        if self._request_worker is not None:  # quit+wait+deleteLater三连
            self._request_worker.quit()
            self._request_worker.wait()
            self._request_worker.deleteLater()
            self._request_worker = None

        self.ui.requestRespBodyTextEdit.clear()
        self.ui.requestRespHeadersTableWidget.setRowCount(0)
        self.ui.requestRespDetailTextEdit.clear()

    def invoke_request(self):
        LOGGER.debug('invoke request -> triggered')
        if self._request_worker is not None and self._request_worker.isRunning():
            LOGGER.warning(f'cannot invoke request, request worker is active')
            return
        self._reset_request_state()
        req = self._gen_request()
        LOGGER.debug(f'invoke request -> req: {req.args()}')
        # request不同阶段的事件(signal),串联到ui层不同的回调
        self._request_worker = RequestWorkerThread(req, parent=self)
        self._request_worker.signals.start.connect(self.on_request_start)
        self._request_worker.signals.progress.connect(self.on_request_progress)
        self._request_worker.signals.finish.connect(self.on_request_finish)
        self._request_worker.start(priority=QThread.Priority.LowPriority)
        LOGGER.debug(f'invoke request -> thread started')

    def on_request_start(self, _):  # 请求开始,此时不能再发起请求
        LOGGER.debug(f'request start')
        self.ui.requestInvokeButton.setEnabled(False)
        self._set_request_status('执行中')
        self._set_request_duration(0)

    def on_request_progress(self, evt: RequestProgressEvent):  # 显示当前耗时,由worker提供
        LOGGER.debug(f'request progress -> seconds: {evt.seconds}')
        self._set_request_duration(evt.seconds)

    def on_request_finish(self, evt: RequestFinishEvent):  # 展示response数据
        LOGGER.debug(f'request finish -> resp: {str(evt.resp)}, err: {evt.err}')

        # set resp status
        if evt.resp is None:
            if evt.err is None:
                self._set_request_status('无响应')
            else:  # 展示worker提供的错误信息
                if isinstance(evt.err, (Timeout, ConnectTimeout, ReadTimeout)):
                    self._set_request_status('请求超时')
                else:
                    self._set_request_status('请求异常')
        else:  # 展示状态码对应文案
            status_code = evt.resp.status_code
            if 200 <= status_code < 300:
                self._set_request_status(f'{status_code} 成功')
            elif 300 <= status_code < 400:
                self._set_request_status(f'{status_code} 重定向')
            elif 400 <= status_code < 500:
                self._set_request_status(f'{status_code} 客户端错误')
            elif 500 <= status_code < 600:
                self._set_request_status(f'{status_code} 服务器错误')
            else:
                self._set_request_status(f'{status_code} 未知')

        # set resp duration
        self._set_request_duration(evt.seconds)

        # set resp body
        if evt.resp is None:
            self.ui.requestRespBodyTextEdit.clear()
        else:
            body = json_pretty(evt.resp.body)
            self.ui.requestRespBodyTextEdit.setText(body)

        # set resp headers
        if evt.resp is None:
            self.ui.requestRespHeadersTableWidget.setRowCount(0)
        else:
            headers = evt.resp.headers
            headers_size = len(headers.keys())
            self.ui.requestRespHeadersTableWidget.setRowCount(headers_size)
            r = 0
            for k in sorted(headers.keys()):
                v = headers[k]
                self.ui.requestRespHeadersTableWidget.setItem(r, 0, QTableWidgetItem(k))
                self.ui.requestRespHeadersTableWidget.setItem(r, 1, QTableWidgetItem(v))
                r += 1

        # set resp detail,审计信息
        detail = self._gen_resp_detail(evt)
        self.ui.requestRespDetailTextEdit.setText(detail)

        # clear all states,请求完毕后,清除关联的request worker
        if self._request_worker:
            self._request_worker.quit()
            self._request_worker.wait()
            self._request_worker.deleteLater()
            self._request_worker = None
        self.ui.requestInvokeButton.setEnabled(True)

而RequestWorker可以这样写:

python 复制代码
# 省略imports

# 用多进程,把request放到其他进程里,防止view层运行python代码阻塞
_REQUEST_POOL = ProcessPoolExecutor(max_workers=3)


def _executor():
    return _REQUEST_POOL


class RequestStartEvent:  # 请求开始的事件
    def __init__(self):
        pass

class RequestProgressEvent:  # 请求中
    def __init__(self, seconds: float):
        self.seconds = seconds

class RequestFinishEvent:  # 请求完成,如果发起失败或者有exception也算完成
    def __init__(self,
                 req: Optional[Request],
                 resp: Optional[Response],
                 err: Optional[Exception],
                 seconds: float):
        self.req = req
        self.resp = resp
        self.err = err
        self.seconds = seconds


class RequestSignals(QObject):  # 定义request的信号(事件)
    start = Signal(RequestStartEvent)
    progress = Signal(RequestProgressEvent)
    finish = Signal(RequestFinishEvent)


class RequestWorkerThread(QThread):
    def __init__(self, req: Request, parent=None):
        QThread.__init__(self, parent)
        self.req = req
        self.signals = RequestSignals()

    def run(self):
        LOGGER.info(f'do request at thread: {str(QThread.currentThread())}')
        req = self.req
        if not isinstance(req, Request):  # 类型不符,直接finish
            evt = RequestFinishEvent(
                req=None,
                resp=None,
                err=ValueError('req must be Request instance'),
                seconds=0
            )
            self.signals.finish.emit(evt)
            return
        validate_err = req.validate()
        if validate_err:  # 校验失败,直接finish
            evt = RequestFinishEvent(
                req=req,
                resp=None,
                err=validate_err,
                seconds=0
            )
            self.signals.finish.emit(evt)
            return
        self.signals.start.emit(RequestStartEvent())  # 开始请求,发事件

        executor = _executor()
        start_time = timeutil.now()
        future = executor.submit(req.invoke)  # submit给executor后返回future,可以随时调用done方法,看invoke是否完成了
        while not future.done():  # 没完成就把当前耗时emit给到view层
            cur_time = timeutil.now()
            seconds = (cur_time - start_time).total_seconds()
            evt = RequestProgressEvent(
                seconds=seconds
            )
            self.signals.progress.emit(evt)
            sleep_ms = random.randint(50, 150)  # sleep一个间隔,防止当前py虚拟机阻塞
            QThread.msleep(sleep_ms)
        resp, err = future.result()  # done了之后取result,即Response实例
        end_time = timeutil.now()
        seconds = (end_time - start_time).total_seconds()
        evt = RequestFinishEvent(
            req=req,
            resp=resp,
            err=err,
            seconds=seconds
        )
        self.signals.finish.emit(evt)  # 完成请求,发事件

这样,通过在worker里把request放到ProcessPoolExecutor里另一个进程,实时监控执行,再结合signal机制防止view层阻塞,就可以把简单的http请求能力串联起来。至于想了解ProcessPoolExecutor怎么运作的,可以参考笔者以前写的这篇ProcessPoolExecutor源码分析文章

当然,作为一个postman工具,通常有把请求内容导出成curl的需求。这个场景下,我们需要用到一个叫做curlify的工具库来达到效果。curlify提供了一个to_curl函数,可以将一个请求实例转化成curl语句:

python 复制代码
def to_curl(request, compressed=False, verify=True):
    parts = [
        ('curl', None),
        ('-X', request.method),
    ]

    for k, v in sorted(request.headers.items()):
        parts += [('-H', '{0}: {1}'.format(k, v))]

    if request.body:
        body = request.body
        if isinstance(body, bytes):
            body = body.decode('utf-8')
        parts += [('-d', body)]

    if compressed:
        parts += [('--compressed', None)]

    if not verify:
        parts += [('--insecure', None)]

    # 下略,主要是组装命令

这里有一个坑点在于,我们有时候把requests转化为curl,得在请求之前去做,而我们一般用requests.get、requests.request时候,其实已经把请求发送出去了。因此,这个情况下我们需要简单探秘一下requests的源码实现,来看需要怎么做才能给到一个请求发送之前的requests实例。

python 复制代码
def request(self):  # requests.session.request,参数略
    req = Request(
        method=method.upper(),
        url=url,
        # 其他参数略
    )
    prep = self.prepare_request(req)


def prepare_request(self, req):
    p = PreparedRequest()
    p.prepare(
        method=request.method.upper(),
        url=request.url,
        files=request.files,
        data=request.data,
        json=request.json,
        headers=merge_setting(
            request.headers, self.headers, dict_class=CaseInsensitiveDict
        ),
        params=merge_setting(request.params, self.params),
        auth=merge_setting(auth, self.auth),
        cookies=merged_cookies,
        hooks=merge_hooks(request.hooks, self.hooks),
    )
    return p

从requests源码中可以看到,在请求之前,会构造一个PreparedRequest实例,来存储所有的请求参数。因此,如果给定请求参数的话,我们也可以显式构造一个PreparedRequest实例,然后调用to_curl函数,将PreparedRequest转化为curl语句,满足在请求发起之间转化的需要。

python 复制代码
def to_curl(self):
    prepared_request = requests.PreparedRequest()
    prepared_request.prepare_method(self.method)
    prepared_request.prepare_url(self.url, None)
    prepared_request.prepare_headers(self.headers)
    prepared_request.prepare_body(self.body.strip(), None)
    return curlify.to_curl(prepared_request)

部署打包

部署方面可以参考官网Deployment文档,采用pyside6-deploy工具来做二进制的部署。部署本身可能需要安装其他的python库,比如Nuitka,这些在自己的python环境里准备就好。部署默认会生成pysidedeploy.spec文件,是部署的配置文件,如果有部署不成功的问题可以改配置文件解决,比如:

  • project_dir:项目路径,相对于你运行部署的路径,填写一个点就行
  • input_file:相对的入口文件路径,比如main.py
  • exec_directory:相对的输出路径,比如output、build、dist之类,按自己情况来
  • python_path:venv可以设置成绝对路径
  • packages:可以指定某些库要安装,如果当前python版本没有对应库的话可以改版本号试试
  • qml_files:相对的qml文件路径。没有qml的话,需要随便指定某个文件,不然会把整个项目include进去,venv开发的话过不了

设置好了跑通之后,预期就会在exec_directory生成二进制文件了。

总结

以上便是通过pyside6开发简单python桌面工具的一整套流程。整体下来,要做好一套pyside6工具,界面注重简洁即可,主要是业务逻辑上确保可独立运行,和view层不要太多耦合。这样整个代码架构会更加灵活,工具内容可复用的潜力也会更多。

相关推荐
HelloRevit35 分钟前
React DndKit 实现类似slack 类别、频道拖动调整位置功能
前端·javascript·react.js
浪里小妖龙37 分钟前
网络爬虫的基础知识
python
晓131337 分钟前
第七章 Python基础进阶-异常、模块与包(其五)
人工智能·python
赖皮猫41 分钟前
PIKIE-RAG 本地部署实践
后端·python·flask
五指山西1 小时前
异步框架使用loguru和contextvars实现日志按Id输出
python
小宁爱Python1 小时前
Python从入门到精通4:计算机网络及TCP网络应用程序开发入门指南
网络·python·tcp/ip·计算机网络
ohMyGod_1231 小时前
用React实现一个秒杀倒计时组件
前端·javascript·react.js
thinkMoreAndDoMore1 小时前
深度学习处理文本(5)
人工智能·python·深度学习
eternal__day1 小时前
第三期:深入理解 Spring Web MVC [特殊字符](数据传参+ 特殊字符处理 + 编码问题解析)
java·前端·spring·java-ee·mvc
醋醋1 小时前
Vue2源码记录
前端·vue.js