关键词: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.py
import 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;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.calculator {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
text-align: center;
backdrop-filter: blur(10px);
}
input {
width: 200px;
padding: 15px;
margin: 10px;
border: none;
border-radius: 8px;
font-size: 16px;
text-align: center;
}
button {
background: #ff6b6b;
color: white;
border: none;
padding: 15px 30px;
margin: 10px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
button:hover {
background: #ff5252;
}
#result {
font-size: 24px;
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
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;
}
pywebview.api.calculate(num1, num2, operation).then(result => {
document.getElementById('result').innerText = `结果:${result}`;
});
}
script>
body>
html>
Python后端逻辑
import webview
import 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()
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>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
}
.header {
background: #2c3e50;
color: white;
padding: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.current-path {
background: #34495e;
padding: 8px 12px;
border-radius: 4px;
font-family: monospace;
}
.toolbar {
background: white;
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
gap: 10px;
}
button {
background: #3498db;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover { background: #2980b9; }
.file-list {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.file-item {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.3s;
}
.file-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.file-icon {
font-size: 40px;
text-align: center;
margin-bottom: 10px;
}
.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 webview
import os
import subprocess
import platform
from 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':
subprocess.run(['open', file_path])
else:
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=(600, 400)
)
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; padding: 20px; }
.todo-item { padding: 10px; margin: 5px 0; background: #f0f0f0; border-radius: 5px; }
.completed { text-decoration: line-through; opacity: 0.6; }
input, button { padding: 10px; margin: 5px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: 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 = { text: this.newTodo, completed: false };
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代码
import webview
import json
import 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的魅力:
学习成本低:会写网页就能做桌面应用
开发效率高
:不用学复杂的GUI框架
界面现代化:告别Tkinter的上世纪风格
跨平台无压力:一套代码到处跑
生态丰富:可以用任何前端框架和库
适合哪些场景?
数据可视化工具
个人效率工具
企业内部管理系统
简单的桌面小应用
原型快速开发
对Python,AI,自动化办公提效,副业发展等感兴趣的伙伴们,扫码添加逍遥,限免交流群
备注【成长交流】