Py学习  »  Python

别再为Python GUI发愁了!这个库让你5分钟就能做出炫酷桌面应用

A逍遥之路 • 3 月前 • 157 次点击  

关键词:Python桌面应用开发、Pywebview教程、Python GUI、Web技术桌面化、跨平台应用开发

还在为Python的桌面应用开发而头疼吗?Tkinter太丑,PyQt太复杂?今天给大家介绍一个宝藏库——Pywebview,让你用熟悉的HTML、CSS、JavaScript就能做出专业级的桌面应用!

🚀 什么是Pywebview?为什么这么香?

说白了,Pywebview就是把网页装进一个独立的桌面窗口里,然后让Python和网页可以互相"聊天"。

想象一下这个场景:

  • 你已经会写网页了(HTML、CSS、JS)

  • 你也会Python编程

  • 但是做桌面应用总是卡壳

Pywebview就是来拯救你的!它让你可以:

  • ✅ 用Web技术做界面(再也不用跟Tkinter的丑UI斗争)

  • ✅ 用Python处理业务逻辑(发挥Python的强大生态)

  • ✅ 一套代码跨三个平台(Windows、Mac、Linux)

  • ✅ 打包后就是原生应用(用户根本看不出是网页)

📦 快速上手:第一个Hello World

安装Pywebview

# 基础安装
pip install pywebview

# 如果你想要更好的性能,可以选择安装额外组件
# Windows用户推荐:
pip install pywebview[cef]

# Linux用户可能需要:
pip install pywebview[gtk]

最简单的示例:打开一个网页

# hello_world.pyimport webview
def main():    # 创建一个窗口,显示百度首页    webview.create_window('我的第一个应用''https://www.baidu.com')    webview.start()
if __name__ == '__main__':    main()

运行这段代码,你就会看到一个独立的窗口打开了百度首页。是不是很简单?

🎨 进阶玩法:用本地HTML做界面

创建本地HTML文件

先创建一个简单的HTML文件:

html><html><head>    <meta charset="UTF-8">    <title>计算器应用title>    <style>        body {            font-family'Microsoft YaHei', Arial, sans-serif;            margin0;            padding20px;            backgroundlinear-gradient(135deg#667eea 0%#764ba2 100%);            color: white;            display: flex;            justify-content: center;            align-items: center;            min-height100vh;        }        .calculator {            backgroundrgba(2552552550.1);            padding30px;            border-radius15px;            box-shadow0 15px 35px rgba(0000.3);            text-align: center;            backdrop-filterblur(10px);        }        input {            width200px;            padding15px;            margin10px;            border: none;            border-radius8px;            font-size16px;            text-align: center;        }        button {            background#ff6b6b;            color: white;            border: none;            padding15px 30px;            margin10px;            


    
border-radius8px;            cursor: pointer;            font-size16px;            transition: background 0.3s;        }        button:hover {            background#ff5252;        }        #result {            font-size24px;            margin20px 0;            padding15px;            backgroundrgba(2552552550.2);            border-radius8px;        }    style>head><body>    <div class="calculator">        <h1>🧮 超级计算器h1>        <input type="number" id="num1" placeholder="输入第一个数字">        <input type="number" id="num2" placeholder="输入第二个数字">        <br>        <button onclick="calculate('add')">➕ 加法button>        <button onclick="calculate('subtract')">➖ 减法button>        <button onclick="calculate('multiply')">✖️ 乘法button>        <button onclick="calculate('divide')">➗ 除法button>        <div id="result">结果将在这里显示div>    div>
    <script>        function calculate(operation) {            const num1 = parseFloat(document.getElementById('num1').value);            const num2 =  parseFloat(document.getElementById('num2').value);
            if (isNaN(num1) || isNaN(num2)) {                document.getElementById('result').innerText = '请输入有效的数字!';                return;            }
            // 调用Python函数            pywebview.api.calculate(num1, num2, operation).then(result => {                document.getElementById('result').innerText = `结果:${result}`;            });        }    script>body>html>

Python后端逻辑

# calculator_app.pyimport webviewimport os
class Api:    def calculate(self, num1, num2, operation):        """处理计算逻辑"""        try:            if operation == 'add':                result = num1 + num2            elif operation == 'subtract':                result = num1 - num2            elif operation == 'multiply':                result = num1 * num2            elif operation == 'divide':                if num2 == 0:                    return "错误:除数不能为0"                result = num1 / num2            else:                return "未知操作"
            return round(result, 2)        except Exception as e:            return f"计算错误:{str(e)}"
def main():    # 创建API实例    api = Api()
    # 获取HTML文件的绝对路径    html_path = os.path.abspath('index.html')
    # 创建窗口    webview.create_window(        title='超级计算器',        url=html_path,        js_api=api,        width=500,        height=600,        resizable=False    )
    # 开启开发者工具(调试时使用)    webview.start(debug=True)
if __name__ == '__main__':    main()

运行这个程序,你就有了一个带炫酷界面的计算器应用!

💡 实战项目:文件管理器

让我们做一个更有用的应用——简单的文件管理器。

HTML界面(file_manager.html)

html><html><head>    <meta charset="UTF-8">    <title>文件管理器title>    <style>        * { margin0padding0box-sizing: border-box; }        body {            font-family'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;            background#f5f5f5;        }        .header {            background#2c3e50;            color: white;            padding15px;            display: flex;            align-items: center;            gap10px;        }        .current-path {            background#34495e;            padding8px 12px;            border-radius4px;            font-family: monospace;        }        .toolbar {            background: white;            padding10px;            border-bottom1px solid #ddd;            display: flex;            gap10px;        }        button {            background#3498db;            color: white;            border: none;            padding8px 16px;            border-radius4px;            cursor: pointer;        }        button:hover { background#2980b9; }        .file-list {            padding20px;            display: grid;            grid-template-columnsrepeat(auto-fill, minmax(200px1fr));            gap15px;        }        .file-item {            background: white;            padding15px;            border-radius8px;            box-shadow0 2px 5px rgba(0,0,0,0.1);            cursor: pointer;            transition: all 0.3s;        }        .file-item:hover {            transformtranslateY(-2px);            box-shadow0 4px 15px rgba(0,0,0,0.2);        }        .file-icon {            font-size40px;


    
            text-align: center;            margin-bottom10px;        }        .file-name {            text-align: center;            font-weight: bold;            word-break: break-all;        }    style>head><body>    <div class="header">        <h1>📁 文件管理器h1>        <div class="current-path" id="currentPath">加载中...div>    div>
    <div class="toolbar">        <button onclick="goBack()">⬅️ 返回上级button>        <button onclick="refreshFiles()">🔄 刷新button>        <button onclick="openFileDialog()">📂 选择文件夹button>    div>
    <div class="file-list" id="fileList">        正在加载文件列表...    div>
    <script>        let currentPath = '';
        // 页面加载时获取当前目录        window.addEventListener('DOMContentLoaded'function() {            refreshFiles();        });
        function refreshFiles() {            pywebview.api.get_current_directory().then(path => {                currentPath = path;                document.getElementById('currentPath').textContent = path;                loadFiles(path);            });        }
        function loadFiles(path) {            pywebview.api.list_files(path).then(files => {                displayFiles(files);            });        }
        function displayFiles( files) {            const fileList = document.getElementById('fileList');            fileList.innerHTML = '';
            files.forEach(file => {                const fileItem = document.createElement('div');                fileItem.className = 'file-item';                fileItem.onclick = () => openItem(file);
                const icon = file.is_directory ? '📁' : getFileIcon(file.name);
                fileItem.innerHTML = `                   
${icon}
                   
${file.name}
                `;
                fileList.appendChild(fileItem);            });        }
        function getFileIcon(filename) {            const ext = filename.split('.').pop().toLowerCase();            const iconMap = {                'txt''📄''doc''📄''docx''📄',                'pdf''📕''jpg''🖼️''jpeg''🖼️'                'png''🖼️''gif''🖼️''mp3''🎵',                'mp4''🎬''zip''📦''rar''📦',                'py''🐍''js''💛''html''🌐'            };            return iconMap[ext] || '📄';        }
        function openItem(file) {            if (file.is_directory) {                loadFiles(file.path);                currentPath = file.path;                document.getElementById('currentPath').textContent = file.path;            } else {                pywebview.api.open_file(file.path);            }        }
        function goBack() {            pywebview.api.get_parent_directory(currentPath).then(parentPath => {                if (parentPath) {                    loadFiles(parentPath);                    currentPath = parentPath;                    document.getElementById('currentPath').textContent = parentPath;                }            });        }
        function openFileDialog() {            pywebview.api.choose_directory().then(path => {                if (path) {                    loadFiles(path);                    currentPath = path;                    document.getElementById('currentPath').textContent = path;                }            });        }    script>body>html>

Python后端(file_manager.py)

import webviewimport osimport subprocessimport platformfrom pathlib import Path
class FileManagerApi:    def get_current_directory(self):        """获取当前工作目录"""        return str(Path.cwd())
    def list_files(self, path):        """列出指定目录下的文件和文件夹"""        try:            items = []            path_obj = Path(path)
            for item in path_obj.iterdir():                items.append({                    'name': item.name,                    'path'str(item.absolute()),                    'is_directory': item.is_dir()                })
            # 按类型排序(文件夹在前,然后按名称排序)            items.sort(key=lambda x: (not x['is_directory'], x['name'].lower()))            return items
        except Exception as e:            return [{'name'f'错误:{str(e)}''path''''is_directory'False}]
    def get_parent_directory(self, path):        """获取父目录"""        try:            parent = Path(path).parent            if parent != Path(path):  # 不是根目录                return str(parent)            return None        except:            return None
    def open_file(self, file_path):        """用系统默认程序打开文件"""        try:            system = platform.system()            if system == 'Windows':                os.startfile(file_path)            elif system == 'Darwin':  # macOS                subprocess.run(['open', file_path])            else:  # Linux                subprocess.run(['xdg-open', file_path])            return True        except Exception as e:            print(f"打开文件失败:{str(e)}")            return False
    def choose_directory(self):        """打开文件夹选择对话框"""        try:            result = webview.windows[0].create_file_dialog(                webview.FOLDER_DIALOG            )            if result:                return result[0]            return None        except:            return None
def main():    api = FileManagerApi()
    # 创建窗口    webview.create_window(        title='文件管理器 v1.0',        url='file_manager.html',        js_api=api,        width=900,        height=700,        min_size=(600400)    )
    webview.start(debug=True)
if __name__ == '__main__':    main()

🔥 高级技巧:与前端框架结合

如果你熟悉Vue.js或React,你完全可以用它们来构建更复杂的界面!

简单的Vue.js集成示例

html><html><head>    <meta charset="UTF-8">    <title>Vue + Pywebviewtitle>    <script src="https://unpkg.com/vue@3/dist/vue.global.js">script>    <style>        body { font-family: Arial, sans-serif; padding20px; }        .todo-item { padding10pxmargin5px 0background#f0f0f0border-radius5px; }        .completed { text-decoration: line-through; opacity0.6; }        inputbutton { padding10pxmargin5pxborder1px solid #dddborder-radius4px; }        button { background#007bffcolor: white; cursor: pointer; }    style>head><body>    <div id="app">        <h1>📝 我的待办清单h1>
        <div>            <input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入新的待办事项">            <button @click="addTodo">添加button>        div>
         <div v-for="(todo, index) in todos" :key="index" class="todo-item" :class="{completed: todo.completed}">            <input type="checkbox" v-model="todo.completed" @change="updateTodo(index)">            {{ todo.text }}            <button @click="deleteTodo(index)" style="float: right; background: #dc3545;">删除button>        div>
        <p>总计:{{ todos.length }} 项,已完成:{{ completedCount }} 项p>    div>
    <script>        const { createApp } = Vue;
        createApp({            data() {                return {                    newTodo'',                    todos: []                }            },            computed: {                completedCount() {                    return this.todos.filter(todo => todo.completed).length;                }            },            methods: {                async addTodo() {                    if (this.newTodo.trim()) {                        const todo = { textthis.newTodocompletedfalse };                        this.todos.push(todo);                        await pywebview.api.save_todos(this.todos);                        this.newTodo = '';                    }                },                async deleteTodo(index) {                    this.todos.splice(index, 1);                    await pywebview.api.save_todos(this.todos);                },                async updateTodo(index) {                    await pywebview.api.save_todos(this.todos);                },                async loadTodos() {                    this.todos = await pywebview.api.load_todos();                }            },            async mounted() {                await this.loadTodos();            }         }).mount('#app');    script>body>html>

对应的Python代码

# vue_todo_app.pyimport webviewimport jsonimport os
class TodoApi:    def __init__(self):        self.data_file = 'todos.json'
    def load_todos(self):        """从文件加载待办事项"""        if os.path.exists(self.data_file):            try:                with open(self.data_file, 'r', encoding='utf-8'as f:                    return json.load(f)            except:                return []        return []
    def save_todos(self, todos):        """保存待办事项到文件"""        try:            with open(self.data_file, 'w', encoding='utf-8'as f:                json.dump(todos, f, ensure_ascii=False, indent=2)            return True        except Exception as e:            print(f"保存失败:{str(e)}")            return False
def main():    api = TodoApi()
    webview.create_window(        title='Vue + Python 待办清单',        url='vue_app.html',        js_api=api,        width=600,        height=500    )
    webview.start(debug=True)
if __name__ == '__main__':    main()

📱 打包发布:让你的应用变成真正的软件

开发完成后,你肯定想把应用打包成exe文件分享给别人。推荐使用PyInstaller:

# 安装PyInstaller
pip install pyinstaller

# 打包成单个exe文件
pyinstaller --onefile--windowed calculator_app.py

# 如果有额外的HTML文件,需要添加到打包中
pyinstaller --onefile --windowed--add-data"index.html;." calculator_app.py

🛠️ 常见问题与解决方案

1. 中文显示乱码

# 在HTML的head中添加
<metacharset="UTF-8">

# Python文件保存为UTF-8编码

2. JavaScript调用Python函数失败

# 确保在create_window时传入了js_api参数
webview.create_window(
    title='我的应用',
    url='index.html',
    js_api=api_instance  # 这个很重要!
)

3. 文件路径问题

import os
# 获取当前脚本所在目录current_dir = os.path.dirname(os.path.abspath(__file__))html_path = os.path.join(current_dir, 'index.html')
webview.create_window('应用', html_path)

🎯 为什么选择Pywebview?

经过这么多实例,相信你已经感受到了Pywebview的魅力:

  1. 学习成本低:会写网页就能做桌面应用

  2. 开发效率高 :不用学复杂的GUI框架

  3. 界面现代化:告别Tkinter的上世纪风格

  4. 跨平台无压力:一套代码到处跑

  5. 生态丰富:可以用任何前端框架和库

适合哪些场景?

  • 数据可视化工具

  • 个人效率工具

  • 企业内部管理系统

  • 简单的桌面小应用

  • 原型快速开发

转发、收藏、在看,是对作者最大的鼓励!👏
关注逍遥不迷路,Python知识日日补!






           对Python,AI,自动化办公提效,副业发展等感兴趣的伙伴们,扫码添加逍遥,限免交流群

备注【成长交流】

图片

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/184143