cef3和tkinter网页浏览器

import os

import sys

import json

import time

import subprocess

import traceback

import tkinter as tk

from tkinter import ttk, messagebox

try:

from cefpython3 import cefpython as cef

except ImportError:

raise SystemExit('请先安装 cefpython3:pip install cefpython3')

APP_DIR = os.path.dirname(os.path.abspath(file))

BOOKMARK_FILE = os.path.join(APP_DIR, 'openlist.json')

SESSION_FILE = os.path.join(APP_DIR, 'session.json')

ERROR_LOG = os.path.join(APP_DIR, 'error.log')

DEFAULT_URL = 'https://www.baidu.com/'

WINDOW_WIDTH = 1200

WINDOW_HEIGHT = 800

DEFAULT_BOOKMARKS = {

'百度一下': 'https://www.baidu.com/',

'必应搜索': 'https://cn.bing.com/'

}

def global_exception_handler(exc_type, exc_value, exc_traceback):

try:

with open(ERROR_LOG, 'a', encoding='utf-8') as f:

f.write(f"\n{time.strftime('%Y-%m-%d %H:%M:%S')}\n")

traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)

except Exception:

pass

sys.exit(1)

sys.excepthook = global_exception_handler

def normalize_url(text):

text = (text or '').strip()

if not text:

return ''

if text.startswith(('http://', 'https://', 'ftp://', 'file://', 'about:')):

return text

if text.startswith('www.'):

return 'http://' + text

return 'https://' + text

def is_allowed_url(url):

url = (url or '').strip()

if not url:

return False

return url.startswith(('http://', 'https://', 'ftp://', 'file://'))

def get_browser_id(browser):

if browser is None:

return None

try:

return browser.GetIdentifier()

except Exception:

return id(browser)

def set_browser_bounds(browser, x, y, width, height):

if not browser:

return

try:

browser.SetBounds(x, y, width, height)

except Exception:

pass

class UrlDialog(tk.Toplevel):

def init(self, parent):

super().init(parent)

self.title('输入网址')

self.transient(parent)

self.grab_set()

self.resizable(True, False)

self.result = None

box = ttk.Frame(self, padding=15)

box.pack(fill='both', expand=True)

ttk.Label(box, text='网址:').grid(row=0, column=0, sticky='e', padx=5, pady=8)

self.url_var = tk.StringVar(value='https://')

entry = ttk.Entry(box, textvariable=self.url_var, width=70)

entry.grid(row=0, column=1, pady=8)

btns = ttk.Frame(box)

btns.grid(row=1, column=0, columnspan=2, pady=10)

ttk.Button(btns, text='访问', command=self.ok).pack(side='left', padx=8)

ttk.Button(btns, text='取消', command=self.cancel).pack(side='left', padx=8)

entry.focus_set()

self.bind('<Return>', lambda e: self.ok())

self.bind('<Escape>', lambda e: self.cancel())

self.update_idletasks()

def ok(self):

url = normalize_url(self.url_var.get())

if not url:

messagebox.showwarning('提示', '请输入有效网址', parent=self)

return

self.result = url

self.destroy()

def cancel(self):

self.destroy()

class BookmarkDialog(tk.Toplevel):

def init(self, parent, title='收藏夹', name='', url=''):

super().init(parent)

self.title(title)

self.transient(parent)

self.grab_set()

self.resizable(True, False)

self.result = None

box = ttk.Frame(self, padding=15)

box.pack(fill='both', expand=True)

ttk.Label(box, text='名称:').grid(row=0, column=0, sticky='e', padx=5, pady=8)

self.name_var = tk.StringVar(value=name)

name_entry = ttk.Entry(box, textvariable=self.name_var, width=35)

name_entry.grid(row=0, column=1, pady=8)

ttk.Label(box, text='网址:').grid(row=1, column=0, sticky='e', padx=5, pady=8)

self.url_var = tk.StringVar(value=url)

url_entry = ttk.Entry(box, textvariable=self.url_var, width=70)

url_entry.grid(row=1, column=1, pady=8)

btns = ttk.Frame(box)

btns.grid(row=2, column=0, columnspan=2, pady=10)

ttk.Button(btns, text='确定', command=self.ok).pack(side='left', padx=8)

ttk.Button(btns, text='取消', command=self.cancel).pack(side='left', padx=8)

name_entry.focus_set()

self.bind('<Return>', lambda e: self.ok())

self.bind('<Escape>', lambda e: self.cancel())

self.update_idletasks()

def ok(self):

name = self.name_var.get().strip()

url = normalize_url(self.url_var.get())

if not name:

messagebox.showwarning('提示', '请输入收藏名称', parent=self)

return

if not url:

messagebox.showwarning('提示', '请输入有效网址', parent=self)

return

self.result = (name, url)

self.destroy()

def cancel(self):

self.destroy()

class Tab(object):

def init(self, notebook, url, app):

self.app = app

self.frame = ttk.Frame(notebook)

self.header = tk.Frame(self.frame, bg='#f0f0f0', height=32)

self.header.pack(fill='x')

self.header.pack_propagate(False)

self.title_label = tk.Label(self.header, text='新标签', bg='#f0f0f0', font=('Microsoft YaHei UI', 10),

anchor='w')

self.title_label.pack(side='left', fill='x', expand=True, padx=10, pady=6)

self.close_btn = tk.Button(self.header, text='X', command=self.close_tab, width=2, bg='#ffcccc', relief='flat',

font=('Arial', 10, 'bold'))

self.close_btn.pack(side='right', padx=8, pady=6)

self.browser_frame = tk.Frame(self.frame, bg='white')

self.browser_frame.pack(fill='both', expand=True)

self.url = url

self.title = '新标签'

self.browser = None

self.browser_id = None

self.closed = False

def close_tab(self):

self.app.close_tab(self)

def set_title(self, title):

if not title:

title = self.url or '新标签'

self.title = title

display_text = title if len(title) <= 20 else title:20 + '...'

self.title_label.config(text=display_text)

class CefClientHandler(object):

def init(self, app):

self.app = app

def call_main(self, func):

try:

self.app.root.after(0, func)

except Exception:

pass

def OnTitleChange(self, *args):

try:

browser = args0

title = args1 if len(args) > 1 else ''

if not isinstance(title, str):

title = str(title)

browser_id = get_browser_id(browser)

self.call_main(lambda b=browser_id, t=title: self.app.update_title(b, t))

except Exception:

pass

def OnAddressChange(self, *args):

try:

browser = args0

frame = args1 if len(args) > 1 else None

url = args2 if len(args) > 2 else ''

if frame and frame.IsMain() and url:

browser_id = get_browser_id(browser)

self.call_main(lambda b=browser_id, u=url: self.app.update_address(b, u))

except Exception:

pass

def OnLoadEnd(self, *args):

try:

browser = args0 if len(args) > 0 else None

frame = args1 if len(args) > 1 else None

self.app.inject_link_hook(browser, frame)

except Exception:

pass

def OnBeforePopup(self, *args):

try:

url = ''

if len(args) >= 3 and isinstance(args2, str):

url = args2

else:

for item in args:

if isinstance(item, str) and item.strip():

url = item

break

url = normalize_url(url)

if url and is_allowed_url(url) and url != 'about:blank':

self.call_main(lambda u=url: self.app.schedule_new_tab(u))

except Exception:

pass

return True

def OnBeforeBrowse(self, *args):

try:

browser = args0

frame = args1

request = args2

if frame and frame.IsMain() and request:

transition = request.GetTransitionType()

try:

tt_link = cef.TT_LINK

except AttributeError:

tt_link = 2

if transition == tt_link:

url = request.GetUrl()

if url and is_allowed_url(url):

if '#' in url:

current_url = browser.GetUrl() if browser else ''

if url.split('#', 1)0 == current_url.split('#', 1)0:

return False

self.call_main(lambda u=url: self.app.schedule_new_tab(u))

return True

except Exception:

pass

return False

class App(object):

def init(self, root):

self.root = root

self.all_tabs = \[\]

self.tabs = {}

self.recent_opens = {}

self.bookmarks = {}

self.closing = False

self.shutdown_done = False

self._shutdown_wait_count = 0

self.load_bookmarks()

self.build_ui()

self.refresh_bookmarks()

self.root.protocol('WM_DELETE_WINDOW', self.on_close_window)

self.root.after(10, self.pump_cef)

if not self.restore_session():

self.add_tab(DEFAULT_URL)

def build_ui(self):

self.root.title('Rogabet 多标签浏览器')

self.root.geometry('{}x{}'.format(WINDOW_WIDTH, WINDOW_HEIGHT))

self.root.minsize(800, 600)

menu_bar = tk.Menu(self.root)

file_menu = tk.Menu(menu_bar, tearoff=0)

file_menu.add_command(label='关闭浏览器', command=self.quit_app)

menu_bar.add_cascade(label='文件', menu=file_menu)

self.root.config(menu=menu_bar)

try:

style = ttk.Style(self.root)

style.configure('.', font=('Microsoft YaHei UI', 11))

except Exception:

pass

top = ttk.Frame(self.root, padding=6)

top.pack(side='top', fill='x')

ttk.Button(top, text='输入网址', command=self.show_url_dialog).pack(side='left', padx=4)

ttk.Button(top, text='新标签', command=lambda: self.add_tab(DEFAULT_URL)).pack(side='left', padx=4)

self.address_var = tk.StringVar()

address_entry = ttk.Entry(top, textvariable=self.address_var, width=55)

address_entry.pack(side='left', fill='x', expand=True, padx=8)

address_entry.bind('<Return>', lambda e: self.open_address_entry())

ttk.Button(top, text='在新标签访问', command=self.open_address_entry).pack(side='left', padx=4)

main = ttk.Frame(self.root, padding=4)

main.pack(fill='both', expand=True)

left = ttk.LabelFrame(main, text='收藏夹', padding=6)

left.pack(side='left', fill='y')

left.config(width=250)

left.pack_propagate(False)

list_bar = ttk.Frame(left)

list_bar.pack(fill='both', expand=True)

self.bookmark_listbox = tk.Listbox(list_bar, font=('Microsoft YaHei UI', 12), activestyle='dotbox')

yscroll = ttk.Scrollbar(list_bar, orient='vertical', command=self.bookmark_listbox.yview)

self.bookmark_listbox.configure(yscrollcommand=yscroll.set)

yscroll.pack(side='right', fill='y')

self.bookmark_listbox.pack(side='left', fill='both', expand=True)

self.bookmark_listbox.bind('<Double-1>', lambda e: self.open_selected_bookmark())

btn_bar = ttk.Frame(left)

btn_bar.pack(fill='x', pady=(8, 0))

buttons = [

('打开', self.open_selected_bookmark),

('新增', self.add_bookmark),

('修改', self.edit_bookmark),

('删除', self.delete_bookmark),

('刷新', self.refresh_bookmarks),

('保存', lambda: self.save_bookmarks(True)),

]

for index, item in enumerate(buttons):

text, command = item

row = index // 2

col = index % 2

ttk.Button(btn_bar, text=text, command=command).grid(row=row, column=col, sticky='ew', padx=3, pady=3)

btn_bar.columnconfigure(0, weight=1)

btn_bar.columnconfigure(1, weight=1)

right = ttk.Frame(main)

right.pack(side='left', fill='both', expand=True)

self.notebook = ttk.Notebook(right)

self.notebook.pack(fill='both', expand=True)

self.notebook.bind('<<NotebookTabChanged>>', self.on_tab_changed)

def on_close_window(self):

pass

def pump_cef(self):

if self.closing or self.shutdown_done:

return

try:

if hasattr(cef, 'MessageLoopWork'):

cef.MessageLoopWork()

except Exception:

pass

self.root.after(10, self.pump_cef)

def load_bookmarks(self):

if os.path.exists(BOOKMARK_FILE):

try:

with open(BOOKMARK_FILE, 'r', encoding='utf-8') as f:

data = json.load(f)

if isinstance(data, dict):

self.bookmarks = data

else:

self.bookmarks = DEFAULT_BOOKMARKS.copy()

except Exception:

self.bookmarks = DEFAULT_BOOKMARKS.copy()

else:

self.bookmarks = DEFAULT_BOOKMARKS.copy()

self.save_bookmarks(False)

def save_bookmarks(self, show_message=False):

try:

with open(BOOKMARK_FILE, 'w', encoding='utf-8') as f:

json.dump(self.bookmarks, f, ensure_ascii=False, indent=2)

if show_message:

messagebox.showinfo('提示', '收藏夹已保存到 openlist.json')

except Exception as error:

messagebox.showerror('保存失败', str(error))

def refresh_bookmarks(self):

self.bookmark_listbox.delete(0, tk.END)

for name in self.bookmarks.keys():

self.bookmark_listbox.insert(tk.END, name)

def open_selected_bookmark(self):

selection = self.bookmark_listbox.curselection()

if not selection:

messagebox.showinfo('提示', '请先选择一个收藏项')

return

name = self.bookmark_listbox.get(selection0)

url = self.bookmarks.get(name, '')

if url:

self.add_tab(url)

def add_bookmark(self):

dlg = BookmarkDialog(self.root, '新增收藏')

self.root.wait_window(dlg)

if not dlg.result:

return

name, url = dlg.result

if name in self.bookmarks:

if not messagebox.askyesno('提示', '收藏名称已存在,是否覆盖?'):

return

self.bookmarksname = url

self.save_bookmarks(False)

self.refresh_bookmarks()

def edit_bookmark(self):

selection = self.bookmark_listbox.curselection()

if not selection:

messagebox.showinfo('提示', '请先选择要修改的收藏项')

return

old_name = self.bookmark_listbox.get(selection0)

old_url = self.bookmarks.get(old_name, '')

dlg = BookmarkDialog(self.root, '修改收藏', old_name, old_url)

self.root.wait_window(dlg)

if not dlg.result:

return

name, url = dlg.result

if name != old_name:

if name in self.bookmarks:

if not messagebox.askyesno('提示', '收藏名称已存在,是否覆盖?'):

return

del self.bookmarksold_name

self.bookmarksname = url

self.save_bookmarks(False)

self.refresh_bookmarks()

def delete_bookmark(self):

selection = self.bookmark_listbox.curselection()

if not selection:

messagebox.showinfo('提示', '请先选择要删除的收藏项')

return

name = self.bookmark_listbox.get(selection0)

if messagebox.askyesno('提示', '确定删除收藏:{} ?'.format(name)):

if name in self.bookmarks:

del self.bookmarksname

self.save_bookmarks(False)

self.refresh_bookmarks()

def show_url_dialog(self):

dlg = UrlDialog(self.root)

self.root.wait_window(dlg)

if dlg.result:

self.add_tab(dlg.result)

def open_address_entry(self):

url = normalize_url(self.address_var.get())

if not url:

messagebox.showinfo('提示', '请输入网址')

return

self.add_tab(url)

self.address_var.set('')

def schedule_new_tab(self, url, source=''):

if self.closing:

return

url = normalize_url(url)

if not url or url == 'about:blank':

return

if not is_allowed_url(url):

return

now = time.time()

old = self.recent_opens.get(url)

if old is not None and now - old < 1.0:

return

self.recent_opensurl = now

if len(self.recent_opens) > 200:

self.recent_opens.clear()

self.add_tab(url)

def add_tab(self, url=''):

if self.closing:

return None

url = normalize_url(url) or DEFAULT_URL

tab = Tab(self.notebook, url, self)

self.all_tabs.append(tab)

self.notebook.add(tab.frame, text=' ', sticky='nsew')

self.notebook.select(tab.frame)

self.address_var.set(url)

self.root.update_idletasks()

self.hide_other_browsers(tab)

self.save_session()

self.root.after(80, lambda: self.create_browser_for_tab(tab))

return tab

def create_browser_for_tab(self, tab):

if self.closing or tab.closed or tab.browser is not None:

return

tab.browser_frame.update_idletasks()

self.root.update_idletasks()

width = tab.browser_frame.winfo_width()

height = tab.browser_frame.winfo_height()

if width <= 1:

width = self.notebook.winfo_width()

if height <= 1:

height = self.notebook.winfo_height()

if width <= 1:

width = 1200

if height <= 1:

height = 800

window_info = cef.WindowInfo()

frame_hwnd = tab.browser_frame.winfo_id()

try:

window_info.SetAsChild(frame_hwnd, 0, 0, width, height)

except Exception:

messagebox.showerror('错误', '创建浏览器窗口失败:SetAsChild 不可用')

self.close_tab(tab)

return

try:

tab.browser = cef.CreateBrowserSync(window_info=window_info, url=tab.url)

except Exception as error:

messagebox.showerror('错误', '创建浏览器失败:{}'.format(str(error)))

self.close_tab(tab)

return

tab.browser_id = get_browser_id(tab.browser)

self.tabstab.browser_id = tab

tab.browser_frame.bind('<Configure>', lambda e, t=tab: self.on_browser_frame_configure(t))

self.hide_other_browsers(tab)

self.show_browser(tab)

self.focus_tab(tab)

tab.set_title(tab.title)

def on_browser_frame_configure(self, tab):

if self.closing or tab.closed or not tab.browser:

return

try:

if self.notebook.select() != str(tab.frame):

return

except Exception:

pass

self.resize_tab(tab)

def resize_tab(self, tab):

if not tab.browser:

return

width = tab.browser_frame.winfo_width()

height = tab.browser_frame.winfo_height()

if width <= 1 or height <= 1:

return

set_browser_bounds(tab.browser, 0, 0, width, height)

def focus_tab(self, tab):

if tab.browser:

try:

tab.browser.SetFocus(True)

except Exception:

pass

def hide_browser(self, tab):

if not tab.browser or tab.closed:

return

try:

tab.browser.SetBounds(-10000, -10000, 1, 1)

except Exception:

pass

def show_browser(self, tab):

if not tab.browser or tab.closed:

return

try:

self.resize_tab(tab)

except Exception:

pass

def hide_other_browsers(self, current_tab):

for tab in self.all_tabs:

if tab.closed:

continue

if current_tab is not None and tab is current_tab:

continue

self.hide_browser(tab)

def on_tab_changed(self, event=None):

selected = None

try:

selected = self.notebook.select()

except Exception:

selected = None

if not selected:

return

current = None

for tab in self.all_tabs:

if tab.closed:

continue

if str(tab.frame) == selected:

current = tab

break

if not current:

return

self.root.update_idletasks()

self.hide_other_browsers(current)

self.show_browser(current)

self.focus_tab(current)

if current.browser:

try:

self.address_var.set(current.browser.GetUrl())

except Exception:

self.address_var.set(current.url)

else:

self.address_var.set(current.url)

def update_title(self, browser_id, title):

tab = self.tabs.get(browser_id)

if not tab or tab.closed:

return

tab.set_title(title)

def update_address(self, browser_id, url):

tab = self.tabs.get(browser_id)

if not tab or tab.closed:

return

tab.url = url

try:

selected_frame = self.notebook.select()

if str(tab.frame) == selected_frame:

self.address_var.set(url)

except Exception:

pass

def inject_link_hook(self, browser, frame):

pass

def close_tab(self, tab):

if tab.closed:

return

tab.closed = True

try:

self.hide_browser(tab)

except Exception:

pass

try:

if tab.browser:

tab.browser.CloseBrowser(True)

tab.browser = None

except Exception:

pass

if tab.browser_id in self.tabs:

del self.tabstab.browser_id

try:

self.notebook.forget(tab.frame)

except Exception:

pass

if tab in self.all_tabs:

self.all_tabs.remove(tab)

self.save_session()

def save_session(self):

urls = \[\]

for tab in self.all_tabs:

if not tab.closed:

url = tab.url

if tab.browser:

try:

current_url = tab.browser.GetUrl()

if current_url and is_allowed_url(current_url):

url = current_url

except Exception:

pass

if url:

urls.append(url)

try:

with open(SESSION_FILE, 'w', encoding='utf-8') as f:

json.dump(urls, f, ensure_ascii=False)

except Exception:

pass

def restore_session(self):

if os.path.exists(SESSION_FILE):

try:

with open(SESSION_FILE, 'r', encoding='utf-8') as f:

urls = json.load(f)

if isinstance(urls, list) and urls:

for url in urls:

self.add_tab(url)

return True

except Exception:

pass

return False

def quit_app(self):

if self.shutdown_done:

return

self.shutdown_done = True

try:

cef.Shutdown()

except Exception:

pass

self.root.destroy()

sys.exit(0)

def end_programr2(pro_name):

os.system('%s%s' % ("taskkill /F /IM ", pro_name))

end_programr2("cefpython3web.exe")

if self.closing:

return

self.closing = True

for tab in list(self.all_tabs):

try:

self.hide_browser(tab)

except Exception:

pass

try:

if tab.browser:

tab.browser.CloseBrowser(True)

tab.browser = None

except Exception:

pass

self.root.after(10, self.wait_for_shutdown)

def wait_for_shutdown(self):

if self.shutdown_done:

return

self._shutdown_wait_count += 1

try:

if hasattr(cef, 'MessageLoopWork'):

cef.MessageLoopWork()

except Exception:

pass

if self._shutdown_wait_count < 50:

self.root.after(10, self.wait_for_shutdown)

else:

self.final_shutdown()

def final_shutdown(self):

if self.shutdown_done:

return

self.shutdown_done = True

try:

cef.Shutdown()

except Exception:

pass

try:

self.root.destroy()

except Exception:

pass

sys.exit(0)

def run_browser():

try:

cef.DpiAware.SetProcessDpiAware()

except Exception:

pass

settings = {

"cache_path": os.path.join(APP_DIR, "cef_cache"),

"persist_session_cookies": True,

}

switches = {

"disable-gpu": "",

"disable-gpu-compositing": "",

"disable-gpu-sandbox": "",

"no-sandbox": "",

}

cef.Initialize(settings=settings, switches=switches)

root = tk.Tk()

app = App(root)

handler = CefClientHandler(app)

try:

cef.SetClientHandler(handler)

except Exception:

pass

root.mainloop()

def main():

if '--child' not in sys.argv:

while True:

cmd = sys.executable, os.path.abspath(__file__), '--child'

result = subprocess.run(cmd)

if result.returncode == 0:

break

time.sleep(1)

else:

run_browser()

if name == 'main':

main()

相关推荐
半兽先生1 小时前
MinerU + LibreOffice 混合架构:搞定 .doc/.ppt 旧格式文档解析与切片
人工智能·python·机器学习·ai·架构
小趴蔡ha1 小时前
03 NumPy 入门:机器学习中的数组和矩阵
python·线性代数·机器学习·numpy
m0_519196401 小时前
【设计模式】java的习题
开发语言·python
pt10432 小时前
网络自动化Python课程:Git版本控制基础入门与实验演示
网络·python·自动化
circuitsosk2 小时前
不止于API调用:大模型推理加速与云原生服务化部署指南
python·云原生·agent·vllm·推理加速·大模型部署·ensorrt-llm
萌动的小火苗2 小时前
深度神经网络中,梯度消失和梯度爆炸的根本原因是什么?有哪些解决方法?【文末含面试万能总结】
人工智能·python·深度学习·神经网络·dnn
卷无止境2 小时前
当Python遇上并发:concurrent.futures的核心逻辑与实战技巧
后端·python
卷无止境2 小时前
编程语言里到底有没有经济学规律?
后端·python
天才测试猿11 小时前
2026软件测试面试八股文(含答案+文档)
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展