诺基亚c601主题(JavaScript 调用提速 40% 的实践)
诺基亚c601主题文章列表:
- 1、JavaScript 调用提速 40% 的实践
- 2、Python和PyQt:构建GUI桌面计算器
- 3、这台SUV颜值我给99分,动力也不弱,途观、CR-V颤抖吧!
- 4、年末逛玩指南,长春频繁上新中!
- 5、看不惯又干不掉,TikTok是如何在美国击败Facebook的
JavaScript 调用提速 40% 的实践
参数适配器机制不仅复杂,而且成本很高。
本文最初发表于 v8.dev(Faster JavaScript calls),基于 CC 3.0 协议分享,由 InfoQ 翻译并发布。
JavaScript 允许使用与预期形式参数数量不同的实际参数来调用一个函数,也就是传递的实参可以少于或者多于声明的形参数量。前者称为申请不足(under-application),后者称为申请过度(over-application)。
在申请不足的情况下,剩余形式参数会被分配 undefined 值。在申请过度的情况下,可以使用 rest 参数和 arguments 属性访问剩余实参,或者如果它们是多余的可以直接忽略。如今,许多 Web/Node.js 框架都使用这个 JS 特性来接受可选形参,并创建更灵活的 API。
直到最近,V8 都有一种专门的机制来处理参数大小不匹配的情况:这种机制叫做参数适配器框架。不幸的是,参数适配是有性能成本的,但在现代的前端和中间件框架中这种成本往往是必须的。但事实证明,我们可以通过一个巧妙的技巧来拿掉这个多余的框架,简化 V8 代码库并消除几乎所有的开销。
我们可以通过一个微型基准测试来计算移除参数适配器框架可以获得的性能收益。
console.time();function f(x, y, z) {}for (let i = 0; i < N; i ) { f(1, 2, 3, 4, 5);}console.timeEnd();
移除参数适配器框架的性能收益,通过一个微基准测试来得出。
上图显示,在无 JIT 模式(Ignition)下运行时,开销消失,并且性能提高了 11.2%。使用 TurboFan 时,我们的速度提高了 40%。
这个微基准测试自然是为了最大程度地展现参数适配器框架的影响而设计的。但是,我们也在许多基准测试中看到了显著的改进,例如我们内部的 JSTests/Array 基准测试(7%)和 Octane2(Richards 子项为 4.6%,EarleyBoyer 为 6.1%)。
太长不看版:反转参数
这个项目的重点是移除参数适配器框架,这个框架在访问栈中被调用者的参数时为其提供了一个一致的接口。为此,我们需要反转栈中的参数,并在被调用者框架中添加一个包含实际参数计数的新插槽。下图显示了更改前后的典型框架示例。
移除参数适配器框架之前和之后的典型 JavaScript 栈框架。
加快 JavaScript 调用
为了讲清楚我们如何加快调用,首先我们来看看 V8 如何执行一个调用,以及参数适配器框架如何工作。
当我们在 JS 中调用一个函数调用时,V8 内部会发生什么呢?用以下 JS 脚本为例:
function add42(x) { return x 42;}add42(3);
在函数调用期间 V8 内部的执行流程。
Ignition
V8 是一个多层 VM。它的第一层称为 Ignition,是一个具有累加器寄存器的字节码栈机。V8 首先会将代码编译为 Ignition 字节码。上面的调用被编译为以下内容:
0d LdaUndefined ;; Load undefined into the accumulator26 f9 Star r2 ;; Store it in register r213 01 00 LdaGlobal [1] ;; Load global pointed by const 1 (add42)26 fa Star r1 ;; Store it in register r10c 03 LdaSmi [3] ;; Load small integer 3 into the accumulator26 f8 Star r3 ;; Store it in register r35f fa f9 02 CallNoFeedback r1, r2-r3 ;; Invoke call
调用的第一个参数通常称为接收器(receiver)。接收器是 JSFunction 中的 this 对象,并且每个 JS 函数调用都必须有一个 this。CallNoFeedback 的字节码处理器需要使用寄存器列表 r2-r3 中的参数来调用对象 r1。
在深入研究字节码处理器之前,请先注意寄存器在字节码中的编码方式。它们是负的单字节整数:r1 编码为 fa,r2 编码为 f9,r3 编码为 f8。我们可以将任何寄存器 ri 称为 fb - i,实际上正如我们所见,正确的编码是- 2 - kFixedFrameHeaderSize - i。寄存器列表使用第一个寄存器和列表的大小来编码,因此 r2-r3 为 f9 02。
Ignition 中有许多字节码调用处理器。可以在此处查看它们的列表。它们彼此之间略有不同。有些字节码针对 undefined 的接收器调用、属性调用、具有固定数量的参数调用或通用调用进行了优化。在这里我们分析 CallNoFeedback,这是一个通用调用,在该调用中我们不会积累执行过程中的反馈。
这个字节码的处理器非常简单。它是用 CodeStubAssembler 编写的,你可以在此处查看。本质上,它会尾调用一个架构依赖的内置 InterpreterPushArgsThenCall。
这个内置方法实际上是将返回地址弹出到一个临时寄存器中,压入所有参数(包括接收器),然后压回该返回地址。此时,我们不知道被调用者是否是可调用对象,也不知道被调用者期望多少个参数,也就是它的形式参数数量。
内置 InterpreterPushArgsThenCall 执行后的框架状态。
最终,执行会尾调用到内置的 Call。它会在那里检查目标是否是适当的函数、构造器或任何可调用对象。它还会读取共享 shared function info 结构以获得其形式参数计数。
如果被调用者是一个函数对象,它将对内置的 CallFunction 进行尾部调用,并在其中进行一系列检查,包括是否有 undefined 对象作为接收器。如果我们有一个 undefined 或 null 对象作为接收器,则应根据 ECMA 规范对其修补,以引用全局代理对象。
执行随后会对内置的 InvokeFunctionCode 进行尾调用。在没有参数不匹配的情况下,InvokeFunctionCode 只会调用被调用对象中字段 Code 所指向的内容。这可以是一个优化函数,也可以是内置的 InterpreterEntryTrampoline。
如果我们假设要调用的函数尚未优化,则 Ignition trampoline 将设置一个 IntepreterFrame。你可以在此处查看V8 中框架类型的简短摘要。
接下来发生的事情就不用多谈了,我们可以看一个被调用者执行期间的解释器框架快照。
我们看到框架中有固定数量的插槽:返回地址、前一个框架指针、上下文、我们正在执行的当前函数对象、该函数的字节码数组以及我们当前正在执行的字节码偏移量。最后,我们有一个专用于此函数的寄存器列表(你可以将它们视为函数局部变量)。add42 函数实际上没有任何寄存器,但是调用者具有类似的框架,其中包含 3 个寄存器。
如预期的那样,add42 是一个简单的函数:
25 02 Ldar a0 ;; Load the first argument to the accumulator40 2a 00 AddSmi [42] ;; Add 42 to itab Return ;; Return the accumulator
请注意我们在 Ldar(Load Accumulator Register)字节码中编码参数的方式:参数 1(a0)用数字 02 编码。实际上,任何参数的编码规则都是[ai] = 2 parameter_count - i - 1,接收器[this] = 2 parameter_count,或者在本例中[this] = 3。此处的参数计数不包括接收器。
现在我们就能理解为什么用这种方式对寄存器和参数进行编码。它们只是表示一个框架指针的偏移量。然后,我们可以用相同的方式处理参数/寄存器的加载和存储。框架指针的最后一个参数偏移量为 2(先前的框架指针和返回地址)。这就解释了编码中的 2。解释器框架的固定部分是 6 个插槽(4 个来自框架指针),因此寄存器零位于偏移量-5 处,也就是 fb,寄存器 1 位于 fa 处。很聪明是吧?
但请注意,为了能够访问参数,该函数必须知道栈中有多少个参数!无论有多少参数,索引 2 都指向最后一个参数!
Return 的字节码处理器将调用内置的 LeaveInterpreterFrame 来完成。该内置函数本质上是从框架中读取函数对象以获取参数计数,弹出当前框架,恢复框架指针,将返回地址保存在一个暂存器中,根据参数计数弹出参数并跳转到暂存器中的地址。
这套流程很棒!但是,当我们调用一个实参数量少于或多于其形参数量的函数时,会发生什么呢?这个聪明的参数/寄存器访问流程将失败,我们该如何在调用结束时清理参数?
参数适配器框架
现在,我们使用更少或更多的实参来调用 add42:
add42();add42(1, 2, 3);
JS 开发人员会知道,在第一种情况下,x 将被分配 undefined,并且该函数将返回 undefined 42 = NaN。在第二种情况下,x 将被分配 1,函数将返回 43,其余参数将被忽略。请注意,调用者不知道是否会发生这种情况。即使调用者检查了参数计数,被调用者也可以使用 rest 参数或 arguments 对象访问其他所有参数。实际上,在 sloppy 模式下甚至可以在 add42 外部访问 arguments 对象。
如果我们执行与之前相同的步骤,则将首先调用内置的 InterpreterPushArgsThenCall。它将像这样将参数推入栈:
内置 InterpreterPushArgsThenCall 执行后的框架状态。
继续与以前相同的过程,我们检查被调用者是否为函数对象,获取其参数计数,并将接收器补到全局代理。最终,我们到达了 InvokeFunctionCode。
在这里我们不会跳转到被调用者对象中的 Code。我们检查参数大小和参数计数之间是否存在不匹配,然后跳转到 ArgumentsAdaptorTrampoline。
在这个内置组件中,我们构建了一个额外的框架,也就是臭名昭著的参数适配器框架。这里我不会解释内置组件内部发生了什么,只会向你展示内置组件调用被调用者的 Code 之前的框架状态。请注意,这是一个正确的 x64 call(不是 jmp),在被调用者执行之后,我们将返回到 ArgumentsAdaptorTrampoline。这与进行尾调用的 InvokeFunctionCode 正好相反。
我们创建了另一个框架,该框架复制了所有必需的参数,以便在被调用者框架顶部精确地包含参数的形参计数。它创建了一个被调用者函数的接口,因此后者无需知道参数数量。被调用者将始终能够使用与以前相同的计算结果来访问其参数,即[ai] = 2 parameter_count - i - 1。
V8 具有一些特殊的内置函数,它们在需要通过 rest 参数或 arguments 对象访问其余参数时能够理解适配器框架。它们始终需要检查被调用者框架顶部的适配器框架类型,然后采取相应措施。
如你所见,我们解决了参数/寄存器访问问题,但是却添加了很多复杂性。需要访问所有参数的内置组件都需要了解并检查适配器框架的存在。不仅如此,我们还需要注意不要访问过时的旧数据。考虑对 add42 的以下更改:
function add42(x) { x = 42; return x;}
现在,字节码数组为:
25 02 Ldar a0 ;; Load the first argument to the accumulator40 2a 00 AddSmi [42] ;; Add 42 to it26 02 Star a0 ;; Store accumulator in the first argument slotab Return ;; Return the accumulator
如你所见,我们现在修改 a0。因此,在调用 add42(1, 2, 3)的情况下,参数适配器框架中的插槽将被修改,但调用者框架仍将包含数字 1。我们需要注意,参数对象正在访问修改后的值,而不是旧值。
从函数返回很简单,只是会很慢。还记得 LeaveInterpreterFrame 做什么吗?它基本上会弹出被调用者框架和参数,直到到达最大形参计数为止。因此,当我们返回参数适配器存根时,栈如下所示:
被调用者 add42 执行之后的框架状态。
我们需要弹出参数数量,弹出适配器框架,根据实际参数计数弹出所有参数,然后返回到调用者执行。
简单总结:参数适配器机制不仅复杂,而且成本很高。
移除参数适配器框架
我们可以做得更好吗?我们可以移除适配器框架吗?事实证明我们确实可以。
我们回顾一下之前的需求:
我们需要能够像以前一样无缝访问参数和寄存器。访问它们时无法进行检查。那成本太高了。
我们需要能够从栈中构造 rest 参数和 arguments 对象。
从一个调用返回时,我们需要能够轻松清理未知数量的参数。
此外,当然我们希望没有额外的框架!
如果要消除多余的框架,则需要确定将参数放在何处:在被调用者框架中还是在调用者框架中。
被调用者框架中的参数
假设我们将参数放在被调用者框架中。这似乎是一个好主意,因为无论何时弹出框架,我们都会一次弹出所有参数!
参数必须位于保存的框架指针和框架末尾之间的某个位置。这就要求框架的大小不会被静态地知晓。访问参数仍然很容易,它就是一个来自框架指针的简单偏移量。但现在访问寄存器要复杂得多,因为它会根据参数的数量而变化。
栈指针总是指向最后一个寄存器,然后我们可以使用它来访问寄存器而无需知道参数计数。这种方法可能行得通,但它有一个关键缺陷。它需要复制所有可以访问寄存器和参数的字节码。我们将需要 LdaArgument 和 LdaRegister,而不是简单的 Ldar。当然,我们还可以检查我们是否正在访问一个参数或寄存器(正或负偏移量),但这将需要检查每个参数和寄存器访问。显然这种方法太昂贵了!
调用者框架中的参数
好的,如果我们在调用者框架中放参数呢?
记住如何计算一个框架中参数 i 的偏移量:[ai] = 2 parameter_count - i - 1。如果我们拥有所有参数(不仅是形式参数),则偏移量将为[ai] = 2 parameter_count - i - 1.也就是说,对于每个参数访问,我们都需要加载实际的参数计数。
但如果我们反转参数会发生什么呢?现在可以简单地将偏移量计算为[ai] = 2 i。我们不需要知道栈中有多少个参数,但如果我们可以保证栈中至少有形参计数那么多的参数,那么我们就能一直使用这种方案来计算偏移量。
换句话说,压入栈的参数数量将始终是参数数量和形参数量之间的最大值,并且在需要时使用 undefined 对象进行填充。
这还有另一个好处!对于任何 JS 函数,接收器始终位于相同的偏移量处,就在返回地址的正上方:[this] = 2。
对于我们的第 1 和第 4 条要求,这是一个干净的解决方案。另外两个要求又如何呢?我们如何构造 rest 参数和 arguments 对象?返回调用者时如何清理栈中的参数?为此,我们缺少的只是参数计数而已。我们需要将其保存在某个地方。只要可以轻松访问此信息即可,具体怎么做没那么多限制。两种基本选项分别是:将其推送到调用者框架中的接收者之后,或被调用者框架中的固定标头部分。我们实现了后者,因为它合并了 Interpreter 和 Optimized 框架的固定标头部分。
如果在 V8 v8.9 中运行前面的示例,则在 InterpreterArgsThenPush 之后将看到以下栈(请注意,现在参数已反转):
内置 InterpreterPushArgsThenCall 执行后的框架状态。
所有执行都遵循类似的路径,直到到达 InvokeFunctionCode。在这里,我们在申请不足的情况下处理参数,根据需要推送尽可能多的 undefined 对象。请注意,在申请过度的情况下,我们不会进行任何更改。最后,我们通过一个寄存器将参数数量传递给被调用者的 Code。在 x64 的情况下,我们使用寄存器 rax。
如果被调用者尚未进行优化,我们将到达 InterpreterEntryTrampoline,它会构建以下栈框架。
没有参数适配器的栈框架。
被调用者框架有一个额外的插槽,其中包含的参数计数可用于构造 rest 参数或 arguments 对象,并在返回到调用者之前清除栈中参数。
返回时,我们修改 LeaveInterpreterFrame 以读取栈中的参数计数,并弹出参数计数和形式参数计数之间的较大数字。
TurboFan
那么代码优化呢?我们来稍微更改一下初始脚本,以强制 V8 使用 TurboFan 对其进行编译:
function add42(x) { return x 42; }function callAdd42() { add42(3); }%PrepareFunctionForOptimization(callAdd42);callAdd42();%OptimizeFunctionOnNextCall(callAdd42);callAdd42();
在这里,我们使用 V8 内部函数来强制 V8 优化调用,否则 V8 仅在我们的小函数变热(经常使用)时才对其进行优化。我们在优化之前调用它一次,以收集一些可用于指导编译的类型信息。在此处阅读有关 TurboFan 的更多信息(https://v8.dev/docs/turbofan)。
这里,我只展示与主题相关的部分生成代码。
movq rdi,0x1a8e082126ad ;; Load the function object <JSFunction add42>push 0x6 ;; Push SMI 3 as argumentmovq rcx,0x1a8e082030d1 ;; <JSGlobal Object>push rcx ;; Push receiver (the global proxy object)movl rax,0x1 ;; Save the arguments count in raxmovl rcx,[rdi 0x17] ;; Load function object {Code} field in rcxcall rcx ;; Finally, call the code object!
尽管这段代码使用了汇编来编写,但如果你仔细看我的注释应该很容易能懂。本质上,在编译调用时,TF 需要完成之前在 InterpreterPushArgsThenCall、Call、CallFunction 和 InvokeFunctionCall 内置组件中完成的所有工作。它应该会有更多的静态信息来执行此操作并发出更少的计算机指令。
带参数适配器框架的 TurboFan
现在,让我们来看看参数数量和参数计数不匹配的情况。考虑调用 add42(1, 2, 3)。它会编译为:
movq rdi,0x4250820fff1 ;; Load the function object <JSFunction add42>;; Push receiver and arguments SMIs 1, 2 and 3movq rcx,0x42508080dd5 ;; <JSGlobal Object>push rcxpush 0x2push 0x4push 0x6movl rax,0x3 ;; Save the arguments count in raxmovl rbx,0x1 ;; Save the formal parameters count in rbxmovq r10,0x564ed7fdf840 ;; <ArgumentsAdaptorTrampoline>call r10 ;; Call the ArgumentsAdaptorTrampoline
如你所见,不难为 TF 添加对参数和参数计数不匹配的支持。只需调用参数适配器 trampoline 即可!
然而这种方法成本很高。对于每个优化的调用,我们现在都需要进入参数适配器 trampoline,并像未优化的代码一样处理框架。这就解释了为什么在优化的代码中移除适配器框架的性能收益比在 Ignition 上大得多。
但是,生成的代码非常简单。从中返回非常容易(结尾):
movq rsp,rbp ;; Clean callee framepop rbpret 0x8 ;; Pops a single argument (the receiver)
我们弹出框架并根据参数计数发出一个返回指令。如果实参计数和形参计数不匹配,则适配器框架 trampoline 将对其进行处理。
没有参数适配器框架的 TurboFan
生成的代码本质上与参数计数匹配的调用代码相同。考虑调用 add42(1, 2, 3)。这将生成:
movq rdi,0x35ac082126ad ;; Load the function object <JSFunction add42>;; Push receiver and arguments 1, 2 and 3 (reversed)push 0x6push 0x4push 0x2movq rcx,0x35ac082030d1 ;; <JSGlobal Object>push rcxmovl rax,0x3 ;; Save the arguments count in raxmovl rcx,[rdi 0x17] ;; Load function object {Code} field in rcxcall rcx ;; Finally, call the code object!
该函数的结尾如何?我们不再回到参数适配器 trampoline 了,因此结尾确实比以前复杂了一些。
movq rcx,[rbp-0x18] ;; Load the argument count (from callee frame) to rcxmovq rsp,rbp ;; Pop out callee framepop rbpcmpq rcx,0x0 ;; Compare arguments count with formal parameter countjg 0x35ac000840c6 < 0x86>;; If arguments count is smaller (or equal) than the formal parameter count:ret 0x8 ;; Return as usual (parameter count is statically known);; If we have more arguments in the stack than formal parameters:pop r10 ;; Save the return addressleaq rsp,[rsp rcx*8 0x8] ;; Pop all arguments according to rcxpush r10 ;; Recover the return addressretl
小结
参数适配器框架是一个临时解决方案,用于实际参数和形式参数计数不匹配的调用。这是一个简单的解决方案,但它带来了很高的性能成本,并增加了代码库的复杂性。如今,许多 Web 框架使用这一特性来创建更灵活的 API,结果带来了更高的性能成本。反转栈中参数这个简单的想法可以大大降低实现复杂性,并消除了此类调用的几乎所有开销。
原文链接:
https://v8.dev/blog/adaptor-frame
延伸阅读:
Deno 2020 年大事记-InfoQ
关注我并转发此篇文章,即可获得学习资料~若想了解更多,也可移步InfoQ官网,获取InfoQ最新资讯~
Python和PyQt:构建GUI桌面计算器
尽管web和移动应用程序似乎已经占领了软件开发市场,但传统的图形用户界面(GUI)桌面应用程序仍然有需求。如果您对用python构建这些类型的应用程序感兴趣,那么您将发现有各种各样的库可供选择。它们包括Tkinter、wxPython、PyQt、PySide和其他一些。
在本教程中,您将学习使用Python和PyQt构建GUI桌面应用程序的基础知识。
在本教程中,您将学习如何:
使用Python和PyQt创建图形用户界面
将应用程序的GUI上的用户事件与应用程序的逻辑连接起来
使用适当的项目布局组织PyQt应用程序
用PyQt创建一个功能齐全的GUI应用程序
在本教程中,您将使用Python和pyqt创建一个计算器应用程序。这个简短的项目将帮助您掌握基础知识,并让您开始使用这个GUI库。
了解PyQt
PyQt是针对Qt的Python绑定,Qt是一组c 库和开发工具,为图形用户界面(gui)提供独立于平台的抽象。Qt还提供了用于网络、线程、正则表达式、SQL数据库、SVG、OpenGL、XML和许多其他强大特性的工具。
由RiverBank Computing Ltd开发的PyQt的最新版本是:
PyQt5:一个只基于Qt5.x构建的版本。
PyQt6:一个只基于Qt6.x构建的版本。
在本教程中,您将使用PyQt6,因为这个版本是该库的未来。从现在开始,一定要将任何提及PyQt的内容视为对PyQt6的引用。
注意:如果您想更深入地了解库的这两个版本之间的区别,请查看关于该主题的PyQt6文档。
PyQt6基于Qt v6。因此,它为GUI创建、XML处理、网络通信、正则表达式、线程、SQL数据库、网页浏览和Qt中可用的其他技术提供了类和工具。PyQt6在一组Python模块中实现了许多Qt类的绑定,这些模块被组织在一个名为PyQt6的顶级Python包中。要使PyQt6工作,您需要Python 3.6.1或更高版本。
PyQt6兼容Windows、Unix、Linux、macOS、iOS和Android。如果您正在寻找一个GUI框架来开发在每个平台上都具有本机外观的多平台应用程序,那么这是一个有吸引力的特性。
PyQt6在两种许可下可用:
The Riverbank Commercial License
The General Public License (GPL), version 3
您的PyQt6许可证必须与您的Qt许可证兼容。如果您使用GPL许可,那么您的代码也必须使用与GPL兼容的许可。如果您想使用PyQt6创建商业应用程序,那么您的安装需要一个商业许可证。
注意:Qt公司已经为Qt库开发并维护了自己的Python绑定。这个Python库被称为Qt for Python,是官方的Qt for Python。它的Python包称为PySide。
PyQt和PySide都建立在Qt之上,它们的API非常相似,因为它们反映了Qt的API。这就是为什么将PyQt代码移植到PySide可以像更新一些导入一样简单。如果你学会了其中一种,那么你就可以用最少的努力来学习另一种。如果您想更深入地了解这两个库之间的区别,那么可以查看PyQt6 vs PySide6。
如果您需要更多关于PyQt6许可的信息,请查看项目官方文档中的许可 FAQs page。
安装PyQt
在系统或开发环境上安装PyQt有几种选择。推荐的选项是使用二元轮。轮子是从Python包索引PyPI安装Python包的标准方法。
在任何情况下,您都需要考虑PyQt6的轮只适用于Python 3.6.1及以后版本。有Linux、macOS和Windows(64位)的轮子。
所有这些轮子都包含相应Qt库的副本,因此不需要单独安装它们。
另一个安装选项是从源代码构建PyQt。这可能有点复杂,所以如果可能的话,您可能想要避免它。如果您确实需要从源代码进行构建,那么请查看库文档在这些情况下的建议。
另外,您可以选择使用包管理器来安装PyQt6,例如Linux上的APT或macOS上的Homebrew。在接下来的几节中,您将了解从不同来源和不同平台上安装PyQt6的一些选项。
使用pip安装虚拟环境
大多数时候,您应该创建一个Python虚拟环境,以一种隔离的方式安装PyQt6。要创建一个虚拟环境并在其中安装PyQt6,在命令行上运行以下命令:
Linux和macOS
$ python -m venv venv$ source venv/bin/activate(venv) $ python -m pip install pyqt6
Windows
PS> python -m venv venvPS> venvScriptsactivate(venv) PS> python -m pip install pyqt6
这里,您首先使用标准库中的venv模块创建一个虚拟环境。然后激活它,最后使用pip在其中安装PyQt6。注意,安装命令必须安装Python 3.6.1或更高版本才能正常工作。
使用pip进行系统级安装
很少需要直接在系统Python环境中安装PyQt。如果您需要进行这种安装,那么在您的命令行或终端窗口中运行以下命令,而不激活任何虚拟环境:
python -m pip install pyqt6
使用此命令,您将直接在系统Python环境中安装PyQt6。您可以在安装完成后立即开始使用库。根据您的操作系统,您可能需要root或管理员权限才能运行此安装。
尽管这是安装PyQt6并立即开始使用它的一种快速方法,但并不推荐使用这种方法。推荐的方法是使用Python虚拟环境,正如您在上一节中所学的那样。
特定于平台的安装
一些Linux发行版在它们的存储库中包含PyQt6的二进制包。如果是这种情况,那么可以使用发行版的包管理器安装库。例如,在Ubuntu上,您可以使用以下命令:
$ sudo apt install python3-pyqt6
使用此命令,您将在基本系统中安装PyQt6及其所有依赖项,因此您可以在任何GUI项目中使用该库。注意,这里需要根权限,您可以在这里使用sudo命令调用根权限。
如果您是macOS用户,那么可以使用Homebrew包管理器安装PyQt6。要做到这一点,打开一个终端并运行以下命令:
$ brew install pyqt6
运行此命令后,您将在您的Homebrew Python环境中安装PyQt6,它就可以供您使用了。
如果在Linux或macOS上使用包管理器,则有可能得不到最新版本的PyQt6。如果您想确保您拥有最新的版本,那么使用pip安装会更好。
创建您的第一个PyQt应用程序
现在您已经有了一个可以工作的PyQt安装,可以创建您的第一个GUI应用程序了。使用Python和PyQt应用程序。以下是你需要遵循的步骤:
从PyQt6.QtWidgets导入QApplication和所有必需的小部件。
创建一个QApplication实例。
创建应用程序的GUI。
显示应用程序的GUI。
运行应用程序的事件循环或主循环。
首先,在当前工作目录中创建一个名为hello.py的新文件:
# hello.py"""Simple Hello, World example with PyQt6."""import sys# 1. Import QApplication and all the required widgetsfrom PyQt6.QtWidgets import QApplication, QLabel, QWidget
首先,导入sys,它将允许您通过exit()函数处理应用程序的终止和退出状态。然后从QtWidgets导入QApplication、QLabel和QWidget, QWidget是PyQt6包的一部分。有了这些导入,第一步就完成了。
要完成第二步,您只需要创建QApplication的一个实例。就像创建任何Python类的实例一样:
# hello.py# ...# 2. Create an instance of QApplicationapp = QApplication([])
在这行代码中,您将创建QApplication的实例。在PyQt中创建任何GUI对象之前,应该先创建应用程序实例。
在内部,QApplication类处理命令行参数。这就是为什么需要将命令行参数列表传递给类构造函数。在本例中,您使用空列表,因为您的应用程序将不处理任何命令行参数。
注意:您经常会发现开发人员传递sys。argv到QApplication的构造函数。该对象包含传入Python脚本的命令行参数列表。如果应用程序需要接受命令行参数,那么应该使用sys。Argv来处理他们。否则,您可以只使用一个空列表,就像在上面的例子中所做的那样。
第三步涉及创建应用程序的GUI。在本例中,您的GUI将基于QWidget类,它是PyQt中所有用户界面对象的基类。
下面是如何创建应用程序的GUI:
# hello.py# ...# 3. Create your application's GUIwindow = QWidget()window.setwindowTitle("PyQt App")window.setGeometry(100, 100, 280, 80)helloMsg = QLabel("<h1>Hello, World!</h1>", parent=window)helloMsg.move(60, 15)
在这段代码中,window是QWidget的一个实例,它提供了创建应用程序窗口或表单所需的所有特性。顾名思义,. setwindowtitle()在应用程序中设置窗口的标题。在本例中,应用程序的窗口将显示PyQt app作为标题。
注意:更准确地说,这一步需要你创建应用程序的顶层或主窗口。术语应用程序的GUI有点通用。通常,应用程序的GUI由多个窗口组成。
您可以使用. setgeometry()来定义窗口的大小和屏幕位置。前两个参数是将放置窗口的x和y屏幕坐标。第三个和第四个参数是窗口的宽度和高度。
每个GUI应用程序都需要小部件或图形化组件来制作应用程序的GUI。在本例中,您使用QLabel小部件helloMsg来显示消息Hello, World!在您的应用程序窗口上。
QLabel对象可以显示html格式的文本,因此可以使用HTML元素“
Hello, World!”
”提供所需的文本作为h1标题。最后,使用.move()将helloMsg放置在应用程序窗口的坐标(60,15)处。
注意:在PyQt中,您可以使用任何小部件—qwidget的子类—作为顶级窗口。唯一的条件是目标小部件不能有父小部件。当您使用小部件作为顶层窗口时,PyQt会自动为它提供一个标题栏,并将其转换为普通窗口。
小部件之间的父子关系有两个互补的目的。没有父窗口的小部件被认为是主窗口或顶层窗口。相比之下,带有显式父部件的小部件是子部件,它显示在其父部件中。
这种关系也被称为所有权,父母拥有他们的孩子。PyQt所有权模型确保如果您删除一个父小部件,例如顶级窗口,那么它的所有子小部件也将自动删除。
为了避免内存泄漏,您应该始终确保任何QWidget对象都有一个父对象(顶级窗口除外)。
你已经完成了第三步,所以你可以继续最后两个步骤,让你的PyQt GUI应用程序准备好运行:
# hello.py# ...# 4. Show your application's GUIwindow.show()# 5. Run your application's event loopsys.exit(app.exec())
在此代码片段中,在window上调用.show()。对.show()的调用调度一个绘制事件,这是一个绘制组成GUI的小部件的请求。然后将此事件添加到应用程序的事件队列中。您将在后面的部分了解更多关于PyQt事件循环的内容。
最后,通过调用.exec()启动应用程序的事件循环。对.exec()的调用被封装在对sys.exit()的调用中,这允许您在应用程序终止时干净地退出Python并释放内存资源。
你可以用以下命令运行你的第一个PyQt应用程序:
$ python hello.py
当你运行这个脚本时,你会看到一个类似这样的窗口:
您的应用程序显示一个基于QWidget的窗口。窗口显示Hello, World!消息。为了显示消息,它使用了一个QLabel小部件。至此,您已经使用PyQt和Python编写了您的第一个GUI桌面应用程序!这不是很酷吗?
考虑到代码风格
如果您检查前一节中示例GUI应用程序的代码,那么您将注意到PyQt的API不遵循PEP 8编码风格和命名约定。PyQt是围绕Qt构建的,Qt是用c 编写的,对函数、方法和变量使用驼峰式命名风格。也就是说,在开始编写PyQt项目时,需要决定使用哪种命名风格。
在这方面,PEP 8指出:
新的模块和包(包括第三方框架)应该按照这些标准编写,但是如果现有的库具有不同的风格,则内部一致性是首选。(Source)
如果您想要编写一致的pyqt相关代码,那么您应该坚持框架的编码风格。在本教程中,为了保持一致性,您将遵循PyQt编码风格。您将使用驼峰情况而不是通常的Python snake case.。
学习PyQt的基础知识
如果您想熟练地使用这个库开发GUI应用程序,则需要掌握PyQt的基本组件。其中一些组成部分包括:
小部件
布局管理器
对话框
主窗口
应用程序
事件循环
信号与插槽
这些元素是任何PyQt GUI应用程序的构建块。它们中的大多数都表示为PyQt6中的Python类。QtWidgets模块。这些因素非常重要。您将在以下几个部分了解更多关于它们的内容。
小部件
小部件是矩形图形化组件,您可以将它们放在应用程序的窗口中以构建GUI。小部件有几个属性和方法,允许您调整它们的外观和行为。他们还可以在屏幕上画出自己的形象。
小部件还检测来自用户、窗口系统和其他来源的鼠标单击、按键和其他事件。每当小部件捕获一个事件时,它都会发出一个信号来宣布其状态更改。PyQt拥有丰富而现代的小部件集合。每个小部件都有不同的用途。
一些最常见和最有用的PyQt小部件是:
按钮
标签
行编辑
组合框
单选按钮
首先是按钮。您可以通过实例化QPushButton来创建按钮,这是一个提供经典命令按钮的类。典型的按钮有“确定”、“取消”、“应用”、“是”、“否”和“关闭”。下面是它们在Linux系统上的样子:
像这样的按钮可能是任何GUI中最常用的小部件。当有人点击它们时,你的应用程序就会命令计算机执行操作。这就是如何在用户单击按钮时执行计算。
接下来是标签,您可以使用QLabel创建标签。标签可以让你以文本或图像的形式显示有用的信息:
你将使用这些标签来解释如何使用你的应用程序的GUI。您可以通过几种方式调整标签的外观。正如您前面看到的,标签甚至可以接受html格式的文本。还可以使用标签指定一个键盘快捷键,将光标焦点移动到GUI上的给定小部件上。
另一个常见的小部件是行编辑,也称为输入框。这个小部件允许您输入单行文本。可以使用QLineEdit类创建行编辑。当需要以纯文本形式获取用户输入时,行编辑非常有用。
下面是Linux系统上的行编辑:
像这样的行编辑自动提供基本的编辑操作,如复制、粘贴、撤消、重做、拖放等。在上图中,您还可以看到第一行上的对象显示占位符文本,以通知用户需要什么样的输入。
组合框是GUI应用程序中另一种基本类型的小部件。您可以通过实例化QComboBox来创建它们。组合框将以一种占用最小屏幕空间的方式为用户提供选项的下拉列表。
下面是一个组合框的例子,它提供了流行编程语言的下拉列表:
这个组合框是只读的,这意味着用户可以从多个选项中选择一个,但不能添加自己的选项。组合框还可以编辑,允许用户动态添加新选项。组合框还可以包含像素图、字符串或两者都包含。
您将了解的最后一个小部件是单选按钮,您可以使用QRadioButton创建它。QRadioButton对象是一个选项按钮,您可以单击它来打开它。当需要用户从多个选项中选择一个时,单选按钮很有用。单选按钮中的所有选项同时显示在屏幕上:
在这个单选按钮组中,在给定的时间内只能选中一个按钮。如果用户选择另一个单选按钮,那么先前选择的按钮将自动关闭。
PyQt有大量的小部件集合。在撰写本文时,有超过40种可供您用于创建应用程序的GUI。在这里,你只研究了一小部分样本。但是,这足以向您展示PyQt的强大功能和灵活性。在下一节中,您将学习如何布局不同的小部件,为您的应用程序构建现代且功能齐全的GUI。
布局管理器
既然您已经了解了小部件以及如何使用它们构建GUI,那么您还需要了解如何安排一组小部件,以使GUI既连贯又有功能。在PyQt中,您将发现一些在窗体或窗口中布局小部件的技术。例如,您可以使用.resize()和.move()方法来给出小部件的绝对大小和位置。
然而,这种技术可能有一些缺点。你必须:
做很多手工计算来确定每个部件的正确大小和位置。
是否进行额外的计算以响应窗口大小调整事件。
当窗口的布局以任何方式发生变化时,重新做大部分计算。
另一种技术涉及使用. resizeevent()动态计算小部件的大小和位置。在这种情况下,您将遇到与前一种技术类似的头痛。
最有效和推荐的技术是使用PyQt的布局管理器。它们将提高您的工作效率,降低出错的风险,并提高代码的可维护性。
布局管理器是允许您在应用程序窗口或窗体上调整小部件的大小和位置的类。它们自动适应调整事件和GUI更改的大小,控制所有子部件的大小和位置。
注意:如果您开发国际化的应用程序,那么您可能会看到翻译后的文本在句中被截断。当目标自然语言比原始语言更冗长时,这种情况很可能发生。布局管理器可以根据可用空间自动调整小部件的大小,从而帮助您避免这种常见问题。然而,对于特别冗长的自然语言,这个特性有时会失败。
PyQt提供了四个基本的布局管理器类:
QHBoxLayout
QVBoxLayout
QGridLayout
QFormLayout
第一个布局管理器类,QHBoxLayout,从左到右水平排列小部件,就像下图中的假设小部件:
在水平布局中,小部件将一个接一个地出现,从左侧开始。下面的代码示例展示了如何使用QHBoxLayout水平排列三个按钮:
# h_layout.py """Horizontal layout example.""" import sys from PyQt6.QtWidgets import ( QApplication, QHBoxLayout, QPushButton, QWidget,)app = QApplication([])window = QWidget()window.setWindowTitle("QHBoxLayout")layout = QHBoxLayout()layout.addWidget(QPushButton("Left"))layout.addWidget(QPushButton("Center"))layout.addWidget(QPushButton("Right"))window.setLayout(layout)window.show()sys.exit(app.exec())
下面是这个例子如何创建一个水平布局的按钮:
第18行创建一个名为layout的QHBoxLayout对象。
第19至21行通过调用. addwidget()方法向布局添加三个按钮。
第22行使用. setlayout()将布局设置为窗口的布局。
当你从命令行运行python h_layout.py时,你会得到以下输出:
上图显示了三个按钮的水平排列。这些按钮从左到右显示的顺序与您在代码中添加它们的顺序相同。
下一个布局管理器类是QVBoxLayout,它从上到下垂直排列小部件,如下图所示:
每个新的小部件将出现在前一个小部件的下面。这种布局允许您构建垂直布局,并在GUI上从上到下组织您的小部件。
下面是如何创建一个包含三个按钮的QVBoxLayout对象:
# v_layout.py """Vertical layout example.""" import sys from PyQt6.QtWidgets import ( QApplication, QPushButton, QVBoxLayout, QWidget,)app = QApplication([])window = QWidget()window.setWindowTitle("QVBoxLayout")layout = QVBoxLayout()layout.addWidget(QPushButton("Top"))layout.addWidget(QPushButton("Center"))layout.addWidget(QPushButton("Bottom"))window.setLayout(layout)window.show()sys.exit(app.exec())
在第18行,创建一个名为layout的QVBoxLayout实例。在接下来的三行中,为布局添加三个按钮。最后,通过第22行上的. setlayout()方法,使用布局对象将小部件排列为垂直布局。
当你运行这个示例应用程序时,你会看到一个类似这样的窗口:
该图显示了三个按钮的垂直排列,一个在另一个的下面。按钮的出现顺序与您在代码中添加它们的顺序相同,从上到下。
列表中的第三个布局管理器是QGridLayout。该类在行和列的网格中排列小部件。每个小部件在网格上都有一个相对位置。您可以使用一对坐标(row, column)来定义小部件的位置。每个坐标必须是整数。这些坐标对定义了给定小部件将占据网格中的哪个单元格。
网格布局看起来像这样:
QGridLayout利用可用空间,将其划分为行和列,并将每个子部件放入自己的单元格中。
下面是如何在你的GUI中创建一个网格布局:
# g_layout.py """Grid layout example.""" import sys from PyQt6.QtWidgets import ( QApplication, QGridLayout, QPushButton, QWidget,)app = QApplication([])window = QWidget()window.setWindowTitle("QGridLayout")layout = QGridLayout()layout.addWidget(QPushButton("Button (0, 0)"), 0, 0)layout.addWidget(QPushButton("Button (0, 1)"), 0, 1)layout.addWidget(QPushButton("Button (0, 2)"), 0, 2)layout.addWidget(QPushButton("Button (1, 0)"), 1, 0)layout.addWidget(QPushButton("Button (1, 1)"), 1, 1)layout.addWidget(QPushButton("Button (1, 2)"), 1, 2)layout.addWidget(QPushButton("Button (2, 0)"), 2, 0)layout.addWidget( QPushButton("Button (2, 1) 2 Columns Span"), 2, 1, 1, 2)window.setLayout(layout)window.show()sys.exit(app.exec())
在本例中,您将创建一个应用程序,该应用程序使用QGridLayout对象来组织屏幕上的小部件。注意,在本例中,传递给. addwidget()的第二个和第三个参数是整数,定义了每个小部件在网格中的位置。
在第26至28行,您将另外两个参数传递给. addwidget()。这些参数是rowSpan和columnSpan,它们是传递给函数的第四个和第五个参数。可以使用它们使小部件占用多行或多列,就像在示例中所做的那样。
如果你从你的命令行运行这段代码,那么你会看到一个像这样的窗口:
在此图中,可以看到小部件排列在行和列的网格中。最后一个小部件占用两列,正如您在第26至28行中指定的那样。
您将学习的最后一个布局管理器是QFormLayout。这个类以两列布局的方式排列小部件。第一列通常在标签中显示消息。第二列通常包含QLineEdit、QComboBox、QSpinBox等小部件。这允许用户输入或编辑关于第一列中的信息的数据。
下图展示了表单布局在实践中的工作原理:
左列由标签组成,而右列由输入部件组成。如果您正在开发一个数据库应用程序,那么这种布局可能是一个有用的工具,可以提高您在创建输入表单时的工作效率。
下面的例子展示了如何创建一个使用QFormLayout对象来排列小部件的应用程序:
# f_layout.py """Form layout example.""" import sys from PyQt6.QtWidgets import ( QApplication, QFormLayout, QLineEdit, QWidget,)app = QApplication([])window = QWidget()window.setWindowTitle("QFormLayout")layout = QFormLayout()layout.addRow("Name:", QLineEdit())layout.addRow("Age:", QLineEdit())layout.addRow("Job:", QLineEdit())layout.addRow("Hobbies:", QLineEdit())window.setLayout(layout)window.show()sys.exit(app.exec())
在这个例子中,第18到23行完成了困难的工作。QFormLayout有一个叫做. addrow()的方便方法。您可以使用此方法向布局中添加包含两个小部件的行。. addrow()的第一个参数应该是一个标签或字符串。然后,第二个参数可以是允许用户输入或编辑数据的任何小部件。在这个特定的示例中,您使用了行编辑。
如果你运行这段代码,你会看到一个像这样的窗口:
上图显示了一个使用表单布局的窗口。第一列包含向用户询问一些信息的标签。第二列显示允许用户输入或编辑所需信息的小部件。
对话框
使用PyQt,您可以开发两种类型的GUI桌面应用程序。根据您用来创建主窗体或窗口的类的不同,您将拥有以下其中之一:
主窗口样式的应用程序:应用程序的主窗口继承自QMainWindow。
对话框风格的应用程序:应用程序的主窗口继承自QDialog。
您将首先从对话框风格的应用程序开始。在下一节中,您将了解主窗口样式的应用程序。
要开发对话框风格的应用程序,您需要创建一个继承自QDialog的GUI类,它是所有对话框窗口的基类。对话框窗口是一个独立的窗口,您可以将其用作应用程序的主窗口。
注意:在主窗口风格的应用程序中,对话窗口通常用于与用户进行简短的通信和交互。
当您使用对话框窗口与用户通信时,这些对话框可以是:
Modal:阻止对同一应用程序中任何其他可见窗口的输入。你可以通过调用它的.exec()方法来显示模态对话框。
无模态:独立于同一应用程序中的其他窗口操作。你可以使用它的.show()方法来显示一个非模态对话框。
对话框窗口还可以提供返回值并具有默认按钮,如Ok和Cancel。
对话框总是一个独立的窗口。如果一个对话框有一个父窗口,那么它将显示在父窗口部件的顶部。与父级的对话框将共享父级的任务栏条目。如果你没有为一个给定的对话框设置parent,那么这个对话框将在系统的任务栏中获得它自己的条目。
下面是一个如何使用QDialog开发对话框式应用程序的例子:
# dialog.py """Dialog-style application.""" import sys from PyQt6.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QFormLayout, QLineEdit, QVBoxLayout,)class Window(QDialog): def __init__(self): super().__init__(parent=None) self.setWindowTitle("QDialog") dialogLayout = QVBoxLayout() formLayout = QFormLayout() formLayout.addRow("Name:", QLineEdit()) formLayout.addRow("Age:", QLineEdit()) formLayout.addRow("Job:", QLineEdit()) formLayout.addRow("Hobbies:", QLineEdit()) dialogLayout.addLayout(formLayout) buttons = QDialogButtonBox() buttons.setStandardButtons( QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok ) dialogLayout.addWidget(buttons) self.setLayout(dialogLayout)if __name__ == "__main__": app = QApplication([]) window = Window() window.show() sys.exit(app.exec())
这个应用程序稍微复杂一些。以下是这段代码的作用:
第16行通过继承QDialog为应用程序的GUI定义了一个Window类。
第18行使用super()调用父类的.__init__()方法。这个调用允许您正确地初始化该类的实例。在本例中,父参数被设置为None,因为这个对话框将是您的主窗口。
第19行设置窗口的标题。
第20行将一个QVBoxLayout对象赋给dialogLayout。
第21行将一个QFormLayout对象分配给formLayout。
第22至25行向formLayout添加小部件。
第26行调用dialogLayout上的. addlayout()。这个调用将表单布局嵌入到全局对话框布局中。
第27行定义了一个按钮框,它提供了一个方便的空间来显示对话框的按钮。
第28至31行向对话框添加两个标准按钮Ok和Cancel。
第32行通过调用. addwidget()将按钮框添加到对话框中。
if __name__ == "__main__":结构包装了应用程序的主代码。这种条件语句在Python应用程序中很常见。它确保缩进代码只在包含的文件作为程序执行而不是作为模块导入时才会运行。有关此构造的更多信息,请查看if name == " main "在Python中做了什么?
注意:在上面示例的第26行,您将注意到布局管理器可以彼此嵌套。您可以通过在容器布局上调用. addlayout()来嵌套布局,并使用嵌套布局作为参数。
上面的代码示例将显示一个类似这样的窗口:
该图显示了您使用QFormLayout对象创建的GUI,该对象用于排列小部件,并使用QVBoxLayout布局用于应用程序的全局布局。
主窗口
大多数时候,你的GUI应用程序将是主窗口风格的应用程序。这意味着它们将有一个菜单栏、一些工具栏、一个状态栏和一个作为GUI主要元素的中心小部件。对于你的应用来说,有几个对话框来完成依赖于用户输入的次要操作也是很常见的。
您将继承QMainWindow来开发主窗口样式的应用程序。从QMainWindow派生的类的实例被认为是应用程序的主窗口,并且应该是唯一的。
QMainWindow为快速构建应用程序的GUI提供了一个框架。这个类有自己的内置布局,它接受以下图形组件:
组件 | 窗口中的位置 | 描述 |
一个菜单栏 | 顶部 | 保存应用程序的主菜单 |
一个或多个工具栏 | 边上 | 保存工具按钮和其他小部件,如QComboBox、QSpinBox |
一个核心小部件 | 中间 | 保存窗口的中心小部件,它可以是任何类型,包括复合小部件 |
一个或多个dock小部件 | 围绕着中心部件 | 小的,可移动的,可隐藏的窗户 |
一个状态栏 | 底部 | 保持应用程序的状态栏,显示状态信息 |
如果没有中心小部件,就无法创建主窗口。您需要一个中心部件,即使它只是一个占位符。在这种情况下,可以使用QWidget对象作为中心小部件。
可以使用. setcentralwidget()方法设置窗口的中心小部件。主窗口的布局将允许您只有一个中心小部件,但它可以是单个或复合小部件。下面的代码示例向您展示了如何使用QMainWindow创建一个主窗口样式的应用程序:
# main_window.py """Main window-style application.""" import sys from PyQt6.QtWidgets import ( QApplication, QLabel, QMainWindow, QStatusBar, QToolBar,)class Window(QMainWindow): def __init__(self): super().__init__(parent=None) self.setWindowTitle("QMainWindow") self.setCentralWidget(QLabel("I'm the Central Widget")) self._createMenu() self._createToolBar() self._createStatusBar() def _createMenu(self): menu = self.menuBar().addMenu("&Menu") menu.addAction("&Exit", self.close) def _createToolBar(self): tools = QToolBar() tools.addAction("Exit", self.close) self.addToolBar(tools) def _createStatusBar(self): status = QStatusBar() status.showMessage("I'm the Status Bar") self.setStatusBar(status)if __name__ == "__main__": app = QApplication([]) window = Window() window.show() sys.exit(app.exec())
下面是这段代码的工作原理:
第15行创建了一个继承自QMainWindow的类Window。
第16行定义了类初始化式。
第17行调用基类的初始化式。同样,parent参数被设置为None,因为这是应用程序的主窗口,所以它必须没有父窗口。
第18行设置窗口的标题。
第19行将QLabel设置为窗口的中心小部件。
第20至22行调用非公共方法来创建不同的GUI元素:第24至26行使用名为menu的下拉菜单创建主菜单栏。这个菜单将有一个退出应用程序的菜单选项。第28行到31行创建工具栏,它将有一个工具栏按钮,用于退出应用程序。第33至36行创建应用程序的状态栏。
当您使用它们自己的方法实现GUI组件时,就像您在本例中对菜单栏、工具栏和状态栏所做的那样,您正在使您的代码更具可读性和可维护性。
注意:如果你在macOS上运行这个例子,那么你可能会遇到应用程序主菜单的问题。macOS隐藏了某些菜单选项,比如Exit。记住,macOS在屏幕顶部的应用程序条目下显示Exit或Quit选项。
当你运行上面的示例应用程序时,你会看到如下的窗口:
你可以确认,你的主窗口风格的应用程序有以下组件:
一个主菜单通常称为菜单
一个工具栏与退出工具按钮
一个中心小部件,包含一个带有文本消息的QLabel对象
窗口底部有一个状态栏
就是这样!您已经学习了如何使用Python和PyQt构建主窗口样式的应用程序。到目前为止,您已经了解了PyQt的小部件集中一些更重要的图形化组件。在接下来的几节中,您将学习与使用PyQt构建GUI应用程序相关的其他重要概念。
应用程序
在开发PyQt GUI应用程序时,QApplication是最基本的类。该类是任何PyQt应用程序的核心组件。它管理应用程序的控制流以及它的主要设置。
在PyQt中,QApplication的任何实例都是一个应用程序。每个PyQt GUI应用程序必须有一个QApplication实例。这个类的一些职责包括:
处理应用程序的初始化和终结
提供事件循环和事件处理
处理大多数系统范围和应用程序范围的设置
提供对全局信息的访问,例如应用程序的目录、屏幕大小等
解析常用的命令行参数
定义应用程序的外观
提供定位功能
这些只是QApplication的一些核心职责。因此,在开发PyQt GUI应用程序时,这是一个基本类。
QApplication最重要的职责之一是提供事件循环和整个事件处理机制。在下一节中,您将深入了解什么是事件循环以及它是如何工作的。
事件循环
GUI应用程序是事件驱动的。这意味着函数和方法是根据用户操作调用的,比如单击按钮、从组合框中选择项、在文本编辑中输入或更新文本、按下键盘上的一个键等等。这些用户操作通常称为事件。
事件由事件循环处理,也称为主循环。事件循环是一个无限循环,在这个循环中,来自用户、窗口系统和任何其他源的所有事件都被处理和分派。事件循环等待事件发生,然后将其分派去执行某些任务。事件循环将继续工作,直到应用程序终止。
所有GUI应用程序都有一个事件循环。当事件发生时,循环检查它是否为终止事件。在这种情况下,循环结束,应用程序退出。否则,事件将被发送到应用程序的事件队列进行进一步处理,并再次循环。在PyQt6中,可以通过在QApplication对象上调用.exec()来运行应用程序的事件循环。
要使事件触发操作,您需要将事件与想要执行的操作连接起来。在PyQt中,可以用信号和槽机制建立连接,这将在下一节中进行探讨。
信号与插槽
PyQt小部件充当事件捕获器。这意味着每个小部件都可以捕获特定的事件,如鼠标点击、按键等。作为对这些事件的响应,小部件发出一个信号,这是一种宣布其状态发生变化的消息。
信号本身不执行任何动作。如果你想要一个信号触发一个动作,那么你需要把它连接到一个插槽。这是一个函数或方法,它将在发出相关信号时执行操作。可以使用任何Python可调用对象作为插槽。
如果一个信号连接到一个插槽,那么每当信号发出时就调用该插槽。如果一个信号没有连接到任何插槽,那么什么也不会发生,信号将被忽略。信号和槽的一些最相关的特性包括:
一个信号可以连接到一个或多个插槽。
一个信号也可以连接到另一个信号。
一个槽可以连接到一个或多个信号。
你可以使用下面的语法来连接信号和插槽:
widget.signal.connect(slot_function)
这将连接slot_function到widget.signal。从现在开始,每当.signal被触发时,都会调用slot_function()。
下面的代码展示了如何在PyQt应用程序中使用信号和插槽机制:
# signals_slots.py """Signals and slots example.""" import sys from PyQt6.QtWidgets import ( QApplication, QLabel, QPushButton, QVBoxLayout, QWidget,)def greet():16 if msgLabel.text(): msgLabel.setText("") else: msgLabel.setText("Hello, World!")app = QApplication([])window = QWidget()window.setWindowTitle("Signals and slots")layout = QVBoxLayout()button = QPushButton("Greet")button.clicked.connect(greet)28layout.addWidget(button)msgLabel = QLabel("")layout.addWidget(msgLabel)window.setLayout(layout)window.show()sys.exit(app.exec())
在第15行,创建greet(),将其用作插槽。然后在第27行中,将按钮的.clicked信号连接到greeting()。这样,每当用户单击Greet按钮时,就会调用Greet()槽,并且标签对象的文本在Hello, World!和一个空字符串:
当您单击Greet按钮时,Hello, World!消息在应用程序的主窗口上出现和消失。
注意:每个小部件都有自己的一组预定义信号。您可以在小部件的文档中查看它们。
如果你的slot函数需要接收额外的参数,那么你可以使用functools.partial()传递它们。例如,你可以修改greet()来接受一个参数,如下面的代码所示:
# signals_slots.py# ...def greet(name): if msg.text(): msg.setText("") else: msg.setText(f"Hello, {name}")# ...
现在greet()需要接收一个名为name的参数。如果你想把这个新版本的greet()连接到.clicked信号,你可以这样做:
# signals_slots.py"""Signals and slots example."""import sysfrom functools import partial# ...button = QPushButton("Greet")button.clicked.connect(partial(greeting, "World!"))# ...
要使这段代码正常工作,您需要首先从functools导入partial()。对partial()的调用返回一个函数对象,当使用name="World!"调用时,该函数对象的行为与greet()类似。现在,当用户单击按钮时,消息Hello, World!会像以前一样出现在标签上。
注意:您还可以使用lambda函数将信号连接到需要额外参数的槽位。作为练习,请尝试使用lambda而不是functools.partial()来编写上面的示例。
信号和槽机制将用于赋予PyQt GUI应用程序生命。这种机制将允许您将用户事件转换为具体的操作。您可以通过查阅有关该主题的PyQt6文档来更深入地研究信号和槽。
现在您已经了解了PyQt的几个重要概念的基础知识。有了这些知识和库的文档,您就可以开始开发自己的GUI应用程序了。在下一节中,您将构建第一个功能齐全的GUI应用程序。
用Python和PyQt创建一个计算器应用程序
在本节中,您将使用模型-视图-控制器(MVC)设计模式开发一个计算器GUI应用程序。这个模式有三层代码,每一层都有不同的角色:
该模型负责应用程序的业务逻辑。它包含核心功能和数据。在计算器应用程序中,模型将处理输入值和计算。
视图实现了应用程序的GUI。它承载终端用户与应用程序交互所需的所有小部件。视图还接收用户的操作和事件。对于您的示例,视图将是屏幕上的计算器窗口。
控制器连接模型和视图以使应用程序工作。用户的事件或请求被发送到控制器,控制器使模型工作。当模型以正确的格式交付所请求的结果或数据时,控制器将其转发给视图。在计算器应用程序中,控制器将从GUI接收目标数学表达式,要求模型执行计算,并使用结果更新GUI。
以下是GUI计算器应用程序如何工作的一步一步描述:
用户在视图(GUI)上执行操作或请求(事件)。
视图通知控制器用户的操作。
控制器获取用户的请求并查询模型以获得响应。
模型处理控制器的查询,执行所需的计算,并返回结果。
控制器接收模型的响应并相应地更新视图。
用户最终在视图上看到请求的结果。
您将使用这个MVC设计用Python和PyQt构建计算器应用程序。
为PyQt计算器应用程序创建框架
首先,在一个名为pycalc.py的文件中为应用程序实现一个最小的框架。
如果您希望自己编写项目代码,那么可以在当前工作目录中创建pycalc.py。在您最喜欢的代码编辑器或IDE中打开该文件,并键入以下代码:
# pycalc.py """PyCalc is a simple calculator built with Python and PyQt.""" import sys from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget WINDOW_SIZE = 235class PyCalcWindow(QMainWindow): """PyCalc's main window (GUI or view).""" def __init__(self): super().__init__() self.setWindowTitle("PyCalc") self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) centralWidget = QWidget(self) self.setCentralWidget(centralWidget)def main(): """PyCalc's main function.""" pycalcApp = QApplication([]) pycalcWindow = PyCalcWindow() pycalcWindow.show() sys.exit(pycalcApp.exec())if __name__ == "__main__": main()
这个脚本实现了运行基本GUI应用程序所需的所有样板代码。您将使用这个框架来构建计算器应用程序。
下面是这段代码的工作原理:
第5行导入sys。这个模块提供了exit()函数,您将使用它干净地终止应用程序。
第7行从PyQt6.QtWidgets中导入所需的类。
第9行创建一个Python常量,为计算器应用程序保存一个固定的窗口大小(以像素为单位)。
第11行创建PyCalcWindow类来提供应用程序的GUI。注意,这个类继承自QMainWindow。
第14行定义了类初始化式。
第15行调用超类的.__init__()以实现初始化目的。
第16行将窗口标题设置为“PyCalc”。
第17行使用. setfixedsize()给窗口一个固定的大小。这确保了用户在应用程序执行期间不能调整窗口的大小。
第18行和第19行创建一个QWidget对象,并将其设置为窗口的中心小部件。这个对象将是计算器应用程序中所有必需的GUI组件的父组件。
第21行定义了计算器的主要函数。使用这样的main()函数是Python中的最佳实践。这个函数提供应用程序的入口点。在main()中,你的程序执行以下操作:第23行创建一个名为pycalcApp的QApplication对象。第24行创建了应用程序窗口pycalcWindow的一个实例。第25行通过在窗口对象上调用.show()来显示GUI。第26行使用.exec()运行应用程序的事件循环。
最后,第29行调用main()来执行计算器应用程序。当运行上述脚本时,屏幕上会出现以下窗口:
就是这样!您已经成功地为GUI计算器应用程序构建了一个功能齐全的应用程序框架。现在可以继续构建项目了。
完成应用的视图
此时您拥有的GUI看起来并不像计算器。您需要通过添加显示目标数学运算的显示器和表示数字和基本数学运算符的按钮键盘来完成这个GUI。您还将添加表示其他所需符号和操作的按钮,比如清除显示。
首先,你需要像下面的代码一样更新你的导入:
# pycalc.pyimport sysfrom PyQt6.QtCore import Qtfrom PyQt6.QtWidgets import ( QApplication, QGridLayout, QLineEdit, QMainWindow, QPushButton, QVBoxLayout, QWidget,)# ...
您将使用QVBoxLayout布局管理器来进行计算器的全局布局。要排列按钮,您将使用一个QGridLayout对象。QLineEdit类将作为计算器的显示,QPushButton将提供所需的按钮。
现在你可以更新PyCalcWindow的初始化式了:
# pycalc.py# ...class PyCalcWindow(QMainWindow): """PyCalc's main window (GUI or view).""" def __init__(self): super().__init__() self.setWindowTitle("PyCalc") self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) self.generalLayout = QVBoxLayout() centralWidget = QWidget(self) centralWidget.setLayout(self.generalLayout) self.setCentralWidget(centralWidget) self._createDisplay() self._createButtons()# ...
您已经添加了突出显示的代码行。你将使用. generallayout作为应用程序的总体布局。在这个布局中,您将把显示器放在顶部,键盘按钮放在底部的网格布局中。
对. _createdisplay()和. _createbuttons()的调用此时还不能工作,因为您还没有实现这些方法。要修复这个问题,首先编写. _createdisplay()。
回到代码编辑器,像下面这样更新pycalc.py:
# pycalc.py# ...WINDOW_SIZE = 235DISPLAY_HEIGHT = 35class PyCalcWindow(QMainWindow): # ... def _createDisplay(self): self.display = QLineEdit() self.display.setFixedHeight(DISPLAY_HEIGHT) self.display.setAlignment(Qt.AlignmentFlag.AlignRight) self.display.setReadOnly(True) self.generalLayout.addWidget(self.display)# ...
在此代码片段中,首先定义一个新常量来保存以像素为单位的显示高度。然后在PyCalcWindow中定义. _createdisplay()。
要创建计算器的显示,可以使用QLineEdit小部件。然后使用DISPLAY_HEIGHT常量为显示设置35个像素的固定高度。显示的文本将左对齐。最后,显示将是只读的,以防止用户直接编辑。最后一行代码将显示添加到计算器的总体布局中。
接下来,您将实现. _createbuttons()方法,为计算器键盘创建所需的按钮。这些按钮将位于网格布局中,因此您需要一种方法在网格中表示它们的坐标。每个坐标对将由一行和一列组成。要表示坐标对,您将使用列表的列表。每个嵌套列表将表示一行。
现在继续,用以下代码更新pycalc.py文件:
# pycalc.py# ...WINDOW_SIZE = 235DISPLAY_HEIGHT = 35BUTTON_SIZE = 40# ...
在这段代码中,您定义了一个名为BUTTON_SIZE的新常量。您将使用这个常量来提供计算器按钮的大小。在这个特定的例子中,所有的按钮都是一个每边40像素的正方形形状。
通过这个初始设置,您可以编写. _createbuttons()方法。您将使用列表的列表来保存键或按钮及其在计算器键盘上的位置。QGridLayout允许你安排计算器窗口上的按钮:
# pycalc.py# ...class PyCalcWindow(QMainWindow): # ... def _createButtons(self): self.buttonMap = {} buttonsLayout = QGridLayout()keyBoard = [ ["7", "8", "9", "/", "C"], ["4", "5", "6", "*", "("], ["1", "2", "3", "-", ")"], ["0", "00", ".", " ", "="], ] for row, keys in enumerate(keyBoard): for col, key in enumerate(keys): self.buttonMap[key] = QPushButton(key) self.buttonMap[key].setFixedSize(BUTTON_SIZE, BUTTON_SIZE) buttonsLayout.addWidget(self.buttonMap[key], row, col) self.generalLayout.addLayout(buttonsLayout)# ...
首先创建空字典self。buttonMap用于保存计算器按钮。然后,创建一个列表的列表来存储键标签。每一行或嵌套列表将表示网格布局中的一行,而每个键标签的索引将表示布局中相应的列。
然后定义两个for循环。外部循环遍历行,内部循环遍历列。在内部循环中,创建按钮并将它们添加到两个self中。buttonMap buttonsLayout。每个按钮都有一个40x40像素的固定大小,这可以通过. setfixedsize()和BUTTON_SIZE常量来设置。
最后,通过调用. generalayout对象上的. addlayout()将网格布局嵌入到计算器的通用布局中。
注意:当谈到小部件大小时,您很少会在PyQt文档中找到度量单位。测量单位被假定为像素,除非使用QPrinter类,它使用点。
现在,计算器的GUI将优雅地显示显示和按钮。但是,您无法更新显示的信息。你可以通过在PyCalcWindow中添加一些额外的方法来解决这个问题:
方法 | 描述 |
.setDisplayText() | 设置和更新显示的文本 |
.displayText() | 获取当前显示的文本 |
.clearDisplay() | 清除显示的文本 |
这些方法将提供GUI的公共接口,并为您的Python计算器应用程序完成视图类。
下面是一个可能的实现:
# pycalc.py# ...class PyCalcWindow(QMainWindow): # ... def setDisplayText(self, text): """Set the display's text.""" self.display.setText(text) self.display.setFocus() def displayText(self): """Get the display's text.""" return self.display.text() def clearDisplay(self): """Clear the display.""" self.setDisplayText("")# ...
以下是每种方法的具体功能:
. setdisplaytext()使用. settext()来设置和更新显示的文本。它还使用. setfocus()将光标的焦点设置在显示器上。
. displaytext()是一个返回显示当前文本的getter方法。当用户点击计算器键盘上的等号(=)时,应用程序将使用. displaytext()的返回值作为要求值的数学表达式。
. cleardisplay()将显示的文本设置为空字符串(""),以便用户可以引入一个新的数学表达式。每当用户按下计算器面板上的C按钮时,就会触发此方法。
现在您的计算器的GUI已经准备好使用了!当你运行这个应用程序时,你会看到一个如下面的窗口:
您已经完成了计算器的GUI,它看起来非常流畅!然而,如果您尝试进行一些计算,那么计算器将不会像预期的那样响应。这是因为您还没有实现模型和控制器组件。在下一节中,您将编写计算器的模型。
实现计算器的模型
在MVC模式中,模型是负责业务逻辑的代码层。在计算器应用程序中,业务逻辑都是关于基本的数学计算。因此,您的模型将计算用户在计算器的GUI中引入的数学表达式。
计算器的模型还需要处理错误。为此,你将定义以下全局常数:
# pycalc.py# ...ERROR_MSG = "ERROR"WINDOW_SIZE = 235# ...
如果用户引入了无效的数学表达式,则ERROR_MSG常量是用户将在计算器显示器上看到的消息。
有了以上的更改,你就可以编写应用程序的模型了,在这个例子中,它将是一个单独的函数:
# pycalc.py# ...class PyCalcWindow(QMainWindow): # ...def evaluateExpression(expression): """Evaluate an expression (Model).""" try: result = str(eval(expression, {}, {})) except Exception: result = ERROR_MSGreturn result# ...
在evaluateExpression()中,使用eval()对作为字符串的数学表达式求值。如果计算成功,则返回结果。否则,将返回预定义的错误消息。注意,这个函数并不完美。它有几个重要的问题:
try…except块不会捕获特定的异常,因此它使用的是Python中不鼓励使用的实践。
该函数使用eval(),这可能会导致一些严重的安全问题。
您可以自由地修改该函数,使其更加可靠和安全。在本教程中,您将按原样使用该函数,将重点放在实现GUI上。
为计算器创建控制器类
在本节中,您将编写计算器的控制器类。这个类将视图连接到您刚刚编写的模型。您将使用控制器类使计算器执行响应用户事件的操作。
你的控制器类需要执行三个主要任务:
访问GUI的公共接口。
处理数学表达式的创建。
连接所有按钮的。点击信号与适当的插槽。
要执行所有这些操作,稍后将编写一个新的PyCalc类。继续使用以下代码更新pycalc.py:
# pytcalc.pyimport sysfrom functools import partial# ...def evaluateExpression(expression): # ...class PyCalc: """PyCalc's controller class.""" def __init__(self, model, view): self._evaluate = model self._view = view self._connectSignalsAndSlots() def _calculateResult(self): result = self._evaluate(expression=self._view.displayText()) self._view.setDisplayText(result) def _buildExpression(self, subExpression): if self._view.displayText() == ERROR_MSG: self._view.clearDisplay() expression = self._view.displayText() subExpression self._view.setDisplayText(expression) def _connectSignalsAndSlots(self): for keySymbol, button in self._view.buttonMap.items(): if keySymbol not in {"=", "C"}: button.clicked.connect( partial(self._buildExpression, keySymbol) ) self._view.buttonMap["="].clicked.connect(self._calculateResult) self._view.display.returnPressed.connect(self._calculateResult) self._view.buttonMap["C"].clicked.connect(self._view.clearDisplay)# ...
在pycalc.py的顶部,从functools导入partial()。您将使用此函数将信号与需要接受额外参数的方法连接起来。
在PyCalc内部,定义类初始化式,它接受两个参数:应用程序的模型和它的视图。然后将这些参数存储在适当的实例属性中。最后,调用. _connectsignalsandslots()来建立所有必需的信号和插槽连接。
在. _calculateresult()中,使用._evaluate()对用户刚刚输入计算器显示的数学表达式求值。然后在计算器视图上调用. setdisplaytext(),用计算结果更新显示文本。
顾名思义,. _buildexpression()方法负责构建目标数学表达式。为此,该方法将初始显示值与用户在计算器键盘上输入的每个新值连接起来。
最后,. _connectsignalsandslots()方法将所有按钮的.clicked信号与控制器类中的适当slots方法连接起来。
就是这样!控制器类已经准备好了。然而,对于所有这些代码作为一个真正的计算器工作,你需要更新应用程序的main()函数如下所示:
# pytcalc.py# ...def main(): """PyCalc's main function.""" pycalcApp = QApplication([]) pycalcWindow = PyCalcWindow() pycalcWindow.show() PyCalc(model=evaluateExpression, view=pycalcWindow) sys.exit(pycalcApp.exec())
这段代码创建了PyCalc的一个新实例。PyCalc类构造函数的模型参数保存了对evaluateExpression()函数的引用,而视图参数保存了对pycalcWindow对象的引用,该对象提供了应用程序的GUI。现在PyQt计算器应用程序可以运行了。
运行计算器
现在您已经完成了用Python和PyQt编写计算器应用程序,是时候进行现场测试了!如果你从你的命令行运行应用程序,那么你会得到这样的结果:
要使用PyCalc,请用鼠标输入有效的数学表达式。然后,按Enter键或单击等号(=)按钮进行计算,并在计算器的显示器上显示表达式结果。就是这样!您已经使用Python和PyQt开发了第一个功能齐全的GUI桌面应用程序!
额外的工具
PyQt6提供了一组有用的附加工具,可以帮助您构建可靠的、现代的、功能齐全的GUI应用程序。与PyQt相关的一些最显著的工具包括Qt Designer和国际化工具包。
Qt Designer允许您使用拖放界面设计和构建图形用户界面。您可以使用这个工具通过使用屏幕上的表单和拖放机制来设计小部件、对话框和主窗口。下面的动画展示了Qt Designer的一些功能:
Qt Designer使用XML .ui文件来存储GUI设计。PyQt包含一个名为uic的模块来帮助处理.ui文件。您还可以使用名为pyyuic6的命令行工具将.ui文件内容转换为Python代码。
注意:要深入了解Qt Designer并更好地理解如何使用该工具创建图形用户界面,请查看Qt Designer和Python:更快地构建您的GUI应用程序。
PyQt6还提供了一套全面的工具,用于将应用程序国际化为本地语言。pylupdate6命令行工具创建和更新翻译(.ts)文件,该文件可以包含接口字符串的翻译。如果您更喜欢GUI工具,那么您可以使用Qt Linguist创建和更新.ts文件,其中包含接口字符串的翻译。
结论
图形用户界面(GUI)应用程序仍然占据软件开发市场的很大份额。Python提供了一些框架和库,可以帮助您开发现代而健壮的GUI应用程序。
在本教程中,您学习了如何使用PyQt,它是用Python开发GUI应用程序最流行和最可靠的库之一。现在您知道了如何有效地使用PyQt构建现代GUI应用程序。
在本教程中,您已经学习了如何:
使用Python和PyQt构建图形用户界面
将用户的事件与应用程序的逻辑连接起来
使用适当的项目布局组织PyQt应用程序
使用PyQt创建一个真实的GUI应用程序
现在,您可以使用Python和PyQt知识为自己的桌面GUI应用程序注入活力。这不是很酷吗?
虽然PyQt6文档是这里列出的第一个资源,但它的一些重要部分仍然缺失或不完整。幸运的是,您可以使用Qt文档来填补空白。
这台SUV颜值我给99分,动力也不弱,途观、CR-V颤抖吧!
根据中汽协的数据,截至今年6月份,2017年上半年,汽车产销1352.28万辆和1335.39万辆,同比增长4.64%和3.81%,增速比上年同期减缓1.83个百分点和4.33个百分点。其中乘用车产销为1148.27万辆和1125.30万辆,同比增长3.16%和1.61%。与上汽通用、南北大众、东风日产等老牌合资车企相比,来自法国的PSA和韩国的现代-起亚无疑是今年上半年最失意的品牌。
由于萨德的原因,进入2017年以来,韩系品牌在华销量近乎腰斩,不过让人意想不到的是比韩系品牌下跌更加惨烈的是在国内耕耘已久的法系品牌。根据统计,今年上半年PSA集团旗下的合资车企神龙汽车仅取得为14.8万辆,同比下跌了48.2%,同时标致、雪铁龙、DS三大品牌也全面告急,特别是雪铁龙和DS品牌,销量跌幅均超6成,市场表现惨淡。
着眼目前国内车市,SUV无疑是关注度最高的当红车型,纵观国内主流一线车厂,无论是合资巨头还是自主翘楚,各品牌型号的SUV车型毫无疑问地占据了市场的主导。
反观PSA集团,目前雪铁龙品牌仅有一款C3-XR小型SUV在勉强维持,在销量竞争最激烈的紧凑型和中型SUV市场上,由于产品线缺失,在连续多年错失SUV市场红利之后,PSA集团终于在今年开始发力,接连推出标致4008、5008两款SUV企图来力挽狂澜,而作为老牌法系品牌的雪铁龙却只能沦为了不折不扣的看客。
刚刚拿下2017欧洲年度车型的标致4008,在引入国产之后,很快成为了国内紧凑型SUV市场上的一款当红车型,前卫的外观与极具科技感的内饰无疑是其最大的卖点。
随着轿车市场销量的萎缩,在错失SUV浪潮,同时在轿车市场上也接连失意的今天,曾经凭借一款富康红遍大江南北的东风雪铁龙,如今却只能靠着低端的爱丽舍在出租车市场刷存在感,其它车型诸如全新旗舰轿车C6,核心紧凑型家轿C4L等市场表现都差强人意,而整个东风雪铁龙也面临着逐步被边缘化的趋势。
在标致4008、5008两款SUV得到市场的肯定之后,沉寂已久的东风雪铁龙也终于祭出了终极“大杀器”-全新天逸 C5 AIRCROSS!
在今年上海车展上,东风雪铁龙为我们带来了基于EMP2平台打造的天逸 C5 AIRCROSS,这款标致4008的姊妹车型有着如概念车一般的靓丽外形,预计今年9月15日正式上市。
新车的外观设计取材自雪铁龙AirCross概念车,其车身线条、分体式前格栅及分体式前大灯组等都与概念车保持着相同的风格。此外,前保险杠两侧的仿通风口造型也十分个性。
天逸的前格栅分为上下两层,上层格栅连接中央雪铁龙品牌标识与LED日间行车灯,下双层格栅则是与前大灯融为一体,这种分体式头灯设计与之前发布的领克01有着异曲同工之妙,如此细节丰富的前脸设计无疑是天逸最大的亮点所在,同时也让整车拥有了如“概念车”一般的辨识度。
从侧面来看,天逸 C5 AIRCROSS采用了悬浮式车顶设计,垂直的C柱和侧窗边缘镀铬饰条显得格外与众不同。另外车车身包围上,天逸保留了原先概念车的元素,拥有一前一后两个装饰格栅,为整个侧面增色不少。
车尾的设计相比起天马行空的车头来说可谓“内敛”许多,尺寸方面,新车的长宽高分别为4510/1860/1670mm,轴距为2730mm。与互为姊妹车型的标致4008相比,天逸 C5 AIRCROSS在车长和轴距上均与前者保持相同,而车宽和车高则因造型的不同而略有增长。
内饰设计上,天逸 C5 AIRCROSS的内饰风格与雪铁龙C6有些相似,大量采用了圆角矩形元素设计,如中控触摸屏幕、空调出风口等,中控台布局也十分整齐,搭配双色的内饰配色,颇具质感。两幅式方向盘小巧精致,无论是功能性还是手感均数上乘;细节上,中央四新车还提供了独特的座椅纹路装饰,同时车门面板也有矩形元素排列,显得非常时尚。
12.3英寸的全液晶仪表盘视觉效果一流,并且有5种主题可选。除了显示车辆信息之外,还可以显示导航地图。
另外在座椅的设计上,雪铁龙也新车费尽心机的营造着自己的个性。黑灰搭配的双色座椅再加以与车身颜色相搭配的红色线条点缀,红色缝线勾勒出的六边形图案精巧活泼,提升了整个座舱的视觉效果。
在动力方面,新车将搭载与4008相同的1.6T/1.8T两款发动机,二者的最大功率分别为167马力和204马力,传动系统匹配的是6速手自一体变速箱。其中变速挡把的设计与4008相同,旁边辅以多路况模式调节旋钮,不出意外的话全系车型同样也会只有前轮驱动一种驱动形式。
这款自带救世主属性的新车无疑被东风雪铁龙寄予厚望,从产品本身来说,天逸有着鱼与熊掌可兼得的潜质,它似乎可以满足消费者既追求极致个性、又不牺牲任何日常使用的特点。另外不得不拿出来单独强调的是,法国人在汽车造型设计的上的造诣确实是独步全球,正如这台天逸所展现的这样,我们总是可以在第一眼就可以识别出它是一台法国车,
另外作为4008的“姊妹车型”,天逸在对于乘客的体贴上显得更加全面到位,舒适性表现势必会成为天逸的砝码。剩余的悬念不外乎就是今年9月正式投放市场后的官方定价了,在我看来,如果定价合理,相信这台既可以挤压韩日品牌同时又可以紧逼德系豪强的法式英俊小车必将会有自己的生存空间。
年末逛玩指南,长春频繁上新中!
⏳不知不觉,日历已经翻到十二月下旬,仿佛被加速了的2022年即将过去。文旅君温馨提醒:今天是2022年12月25日,星期日。
距离2023年元旦还有7天,距离2023年春节还有28天,2022年就要结束,此刻的长春已为你准备了许多惊喜,这座城市正频繁“上新”中!
△图源丨微博@老李娃子
那么
安排!
眼瞅着“逛、玩、买”的快乐
就要回来了~
文旅君贴心为大家收集整理了
「长春年末逛玩指南」
诚意满满的各种福利
一起来看看!
1
第四届长春冰雪新天地
运冰车在场地里来回穿梭、挥舞着“长臂”的塔吊吊运着冰块、冰雕雪雕师傅们站在高高的冰建和雪雕上精心打磨着作品......
这是位于莲花山生态旅游度假区的长春冰雪新天地施工现场,此时已进入冲刺施工阶段,一座座大型冰建已拔地而起,在夜晚亮起五彩斑斓的灯光。
据工作人员介绍,新雪季,长春冰雪新天地景区占地面积156万平方米,计划总用冰量42万立方米,用雪量28万立方米,包括108个单体冰建、28个单体雪雕。
本届长春冰雪新天地主题为“中华五千年 冰雪贺盛世”。景区为弘扬中华民族传统文化,将以冰雪为载体展现五千年华夏文明,规划了“大团结大统一、改革硕果、大国重器、玉兔贺春、神童探宝、莲花迎新春、冰上大乐园、冰涛大观园”八大区域。
其中,高39米的玉兔奔月主塔、全新造型的激情大滑梯是景区的主要看点。此外,景区还规划了60余万平方米的冰雪游乐空间、22个游玩项目,包括每天2场冰雪演艺、1场冰雪巡游,实现从观赏型向互动游乐型全新转变。
目前,长春冰雪新天地已经完成整体进度约80%,预计在月底与市民游客见面。届时,到这里赏冰雕雪雕、玩冰雪乐园、滑黄金粉雪、住热乎民宿……这个冬天,注定“热”“趣”非凡!
2
第二十一届
中国长春净月潭瓦萨国际滑雪节
(资料图片)
第二十一届中国长春净月潭瓦萨国际滑雪节将于2023年1月4日在长春净月潭国家森林公园举办。
(资料图片)
本次比赛线路设置在长春净月潭滑雪场,起点和终点设置在净月雪世界,参与选手将完成中国瓦萨50公里、中国瓦萨25公里及2.5公里三个组别的竞技。
参赛选手将穿越净月潭冰面、森林及公路,穿行在茫茫林海中,挥动雪杖尽情驰骋,体验越野滑雪带来的快感与无限激情!
(资料图片)
2023中国长春净月潭瓦萨国际滑雪节报名现已开启,准备好与魅力无限的净月潭相约了吗?
◆详情戳链接↓↓↓
https://mp.weixin.qq.com/s/C6mUMcsacTRlHzfmpKy9RA
3
长春北湖国家湿地公园
冬季冰雪节
每逢冬天便是冰雪的主场,在冰雪世界里,孩子们坐着雪圈从雪道上疾驰而下;穿上冰刀,在追逐中体会竞技的快乐;骑上雪地摩托,和家人一起感受冰雪运动的魅力......这个冬天,嗨玩冰雪,就来长春北湖国家湿地公园。
长春北湖国家湿地公园依托丰富的冬季自然资源,打造以“冰雪娱乐 夜游项目”组合的冬季冰雪节活动,策划雪地转转、雪地坦克、雪圈滑道、雪地平衡车、冰上碰碰车、冰上体验区六大娱乐项目。
超大规模嬉雪场地、多个嬉雪项目、多重主题活动,融合趣味性、体验性、娱乐性和互动性于一体,让市民游客在玩冰嬉雪里尽情撒野。
4
国泰·RIOMALL
“粉上长春”特色文旅街区
准备好了吗?国泰·RIOMALL“粉上长春”特色文旅街区震撼来袭!
城市双层独角兽木马,超梦幻网红打卡地,燃动青春力量,赋予春城冬季全新内核。
△图源丨微博@Cyn1124_
美陈片场打造超大粉色独角兽蛋糕——粉芯城堡,带你穿越童话世界;粉市好集打造首届冰雪星空集市,引领全新生活风尚;城市粉色嬉冰场、粉色旋转木马星光熠熠,宛如梦幻十足的粉色王国。国泰·RIOMALL“粉”力全开,就等你来~
5
长春北湖吾悦广场
“幸福喜乐屋”兔年美陈展
12月22日起,新城控股集团长春北湖吾悦广场的“幸福喜乐屋”兔年美陈展,用一场有温度的美陈,讲述关于每一个普通人梦想与回家的故事。
精美动人的小兔子、优惠实在的惊喜、充满香气的美食……
2022年12月22日至2023年2月5日,新城控股集团长春北湖吾悦广场用系列有礼活动带大家共享幸福中国年。
◆详情戳链接↓↓↓
https://mp.weixin.qq.com/s/DR4lvM0_Tl-o_v-TwWuGzw
6
这有山·天宫印象画坊
天宫印象画坊,一个可以带给你艺术与创造独特体验的地方,就在这有山。
在这里,可以体验生活赋予艺术再创造的灵感与力量。
比如制作肌理画,当丙烯与石英砂相融合,在画布上刮出沙沙声,整个过程治愈又放松。
比如折纸玫瑰花束,设计源于想留住对爱的追求,对生活的热情。
比如手工黏土DIY,快乐当然要捏泥巴~
这里可以解锁超多有趣玩法,满足每个人的好奇心。
一个个年末惊喜向我们走来
一个个好消息正在路上
大家要打起精神
外出的朋友做好防护
宅家的朋友吃好喝好
2023,我们要来咯!
(注:以上排名不分先后,均为资料图片,文中内容仅供参考。各场所实际情况、开放时间、游玩咨询、最终解释权等以各官方平台发布为准)
◆素材来源:长春文旅综合整理,部分素材来自這有山、长春日报、长春天定山订阅号、长春新区、诺迪维瓦萨 、长春北湖吾悦广场服务号、微博@老李娃子、微博@Cyn1124_和网络,版权归原作者所有,如有版权问题,请联系我们。转载本文请注明“长春文旅”。
◆本期编辑:格子
◆值班主任:柳絮
◆编辑:琳琳
看不惯又干不掉,TikTok是如何在美国击败Facebook的
萧箫 发自 凹非寺
量子位 报道 | 公众号 QbitAI
TikTok,海外版抖音,字节跳动出品。
这是中国科技公司有史以来最成功的全球化产品——甚至没有之一。
而且它发展壮大于巨头扎堆的领域——海外的Facebook、谷歌,Snapchat,哪一个不是根基深厚的巨头,却可以异军突起,风靡于海外用户之间。
却还能在一众围追堵截中——从Facebook扎克伯克的明抢,到标榜公平的美国印度政府的政令,顽强生长。
真是一个不折不扣的“看不惯又干不掉”的产品。
所以问题也来了,TikTok,海外版抖音,发展壮大的核心秘诀究竟是啥?
三年时间,碾压海外知名社交平台
一方面,自然还是打铁还靠本身硬。
先从直观下载量来看,2017年上线的TikTok,在SensorTower的下载量基本一路拔高。
其中,关键爆发节点,发生在当年11月,那时候字节跳动完成了对Musical.ly的收购合并。
△ 图源猎云网
此后,虽然TikTok在海外分别受到了不同国家的政策制裁(FTC控告TikTok非法收集儿童信息等),导致其在2019年前两个季度呈现下滑趋势;
但整体而言,下载量依旧稳定,并在短短3年的时间内积攒了4亿多用户。
从今年2月份的下载排名数据来看,TikTok在印度宣布封禁前,依旧保持着极高的热度。
美国 Our Favorites 分类第一英国 Photo & Video 和Social Networking 分类第一加拿大 Music 分类第二法国 Music 分类 第五
2017年8月才正式上线的TikTok,只利用短短3年的时间,就击败了Facebook旗下Instagram,以及谷歌系的Youtube等老牌主流社交平台。
即使国外政府亲自出手打压,似乎也无法阻止TikTok在海外一众大厂林立的情况下势头疯涨。
所以,TikTok凭什么杀出重围?
一、张一鸣敢赌
TikTok在海外的大火,要从今日头条对Musical.ly的收购说起。
这款堪称“美国青少年版快手”的短视频软件,历时一年就登上了苹果商店在美国地区的榜首,潜力无限。
而张一鸣一眼看中了这个短视频社交软件,并直接在17年11月以10亿美元的资本收购了它。
事实上,快手和Fackbook此前都曾与Musical.ly相关方讨论过收购事宜,但均未能达成协议。
Musical.ly的投资方,来自猎豹的傅盛,提出了一个「捆绑销售」的要求,要求在收购Musical.ly的同时,也要将猎豹下的News Republic和Live.me一同收购,加起来近10亿美元。
要知道,当时外界对Musical.ly的估值仅在5亿美元左右。
快手拒绝了,但张一鸣果断地应了下来。
现在看来,这是一个明确的决定,相比于TikTok单打独斗进驻欧美市场,Musical.ly在海外打下的「江山」无疑于是本土化最好的根基。
用户裂变的来源有了,而TikTok自身产品的特性,决定了它能带来「病毒式转发」的效应。
二、TikTok会玩
在产品内容上,TikTok充分放大了「人类的本质是复读机」的模仿行为,将国外的meme文化发扬到了极致。
meme文化,在青少年群体中指模仿行为,通过形式上的模仿、复制、变异、传播,将文化传播出去,过程中产生的沙雕效果,给传播和受众带来了无穷乐趣,如下图。
△ 图源今日memes
在传播过程中,TikTok的软件做了三个创新举动:将主题标签设为一级分类;提供傻瓜式操作模板;允许用户上传自己的音乐。
想象一下,三步就能在TikTok上跟进潮流、展示自我:
第一步,在搜索框内一键搜索,立即搜到感兴趣标签下的视频内容;第二步,想要录制视频、跟风标签内容时,只需要选取录制模板,就能自带修剪等大片特效;最后,上传自己的音乐,保留用户个性。
用户基本只需动动手指,就能完成原本需要专业剪辑软件才能做出的视频效果。
而在这些标签化下面,隐藏的是大量的视频关联算法,将每一个故事都变得有关联。
除此之外,TikTok在细节上的设计也引人注目。
播放形式上,视频采取沉浸式、竖屏播放,前者让用户放弃思考不需要做出选择,就能根据算法看见自己想要的视频类型;后者则使得用户无需再锻炼颈椎翻转屏幕查看内容,更快捷方便。
视频创作上,用户能直接从创作者的TikTok主页跳到Youtube和Ins,这对创作者的发展非常友好。
三大利器,将TikTok的发展直接推上了顶峰。
哪怕是扎克伯格,对TikTok也做出了非常高的评价,言语间充满忌惮:
我确实认为TikTok很值得关注,不仅仅因为它是最新的潮流,也不仅因为它的扩张态势——从地缘政治方面看,他们的所作所为也很值得思考。
但TikTok的壮大,也意味着Facebook等国际巨头的“失手”,而且Facebook并非在短视频方面没有尝试。
可为啥他们失败了?
Facebook等大公司为啥没火?
「Facebook没有梦想」
Facebook并不是没有做出过尝试的举动。
TikTok火了后一年,Facebook模仿TikTok,推出了独立视频应用软件Lasso。
尴尬的是,上线一年后,Lasso的的下载量仅为42.5万次,《纽约时报》对此做出的评价是「一个蹩脚的抄袭版本」。
而Facebook旗下的子公司Instagram,曾尝试在南美推出过一款短视频软件,也以业绩不好而腰斩。
但事实上,作为美国数一数二的互联网公司,Facebook远非表面上那般风光。
可以说,Facebook近年的发展道路足以谱一曲国外的《脸书没有梦想》。
在与Twitter和Snapchat竞争社交软件市场的过程中,Facebook将「CV玩家」(Ctrl C、Ctrl V)的精髓发挥到了极致:隔壁做出的好功能,直接照搬过来。
Facebook内部的某个团队甚至形成了这样一种风气:「不抄袭也不是啥值得骄傲的事情」。
而扎克伯格本人,在Facebook涉及侵犯用户隐私安全、面临罚款时,第一时间想到的竟然是将TikTok拖下水……
要是说Facebook更多的是自身出了问题,那么再来说说Youtube这些大公司,为什么也没能创造出像TikTok这样的软件。
冲劲难寻
事实上,核心原因也许在于,初创公司的创意有时候更接近于「草率的创业」。
例如,谷歌的最初业务是从做好搜索引擎开始,Instagram的产品想法则是「希望手机能拥有这一功能」,而TikTok也一样,最初Musical.ly创始人的想法只是将青少年喜欢听歌和录视频这两点融合起来。
但大公司必须更加关注业务的数据和竞争,有时候也许就会误判新产品对于公司的贡献预期。
不仅如此,并非所有大公司的文化都适合互联网潮流里最新的创意。
此前,在Instagram爆火的时候,Google就曾推出过社交产品Buzz,但用户体验不好,原因是Google的产品文化更侧重于功能,相比之下,Instagram对于人的情感则会更加照顾。
类似的情况也可以解释如今部分大公司无法再造一个TikTok的原因。
但如今TikTok在国外面临着封杀的情况,这是否是某些视频、社交产品的契机,似乎也不一定。
TikTok与字节跳动如何破圈
日前,国外各大社交平台如Snapchat、Ins、Youtube,已经在寻求进一步发展的契机。
据了解,一些模仿TikTok的短视频软件近日下载量暴增。
其中印度的一个本土竞品短视频应用Chingari,在TikTok下架后一天,每小时暴增20万次下载量。
字节跳动正考虑分拆TikTok为美国公司,并保证不会与国内公司共享任何相关信息。
但此举是否能赢得美国政府的信任,目前尚不得而知。
现在看来,这场战役还会持续下去。
参考链接:
https://stratechery.com/2020/the-tiktok-war/
https://mp.weixin.qq.com/s/vkH2B9AAwCubMiWfQbmkQw
http://www.woshipm.com/it/3443070.html
https://mp.weixin.qq.com/s/on0C6GElAJEbJKZmpOLzzw
https://mp.weixin.qq.com/s/YmePzcRMzXneou-dyk_DW
ghttps://www.sohu.com/a/129975261_350699
— 完 —
量子位 QbitAI · 头条号签约
关注我们,第一时间获知前沿科技动态