前言
这个东西只适用余管理混乱,it信息化建设落后,人员还未开智的地方。废话不多说直接上干货。
如果你只需要打印机连接脚本直接跳到:打印机连接脚本 目录就可以了。
效果和功能
功能:
用户界面(颜色ui可以自己改,就是代码编辑的改起来麻烦):
前期准备和须知
系统环境:
python3.13
windwos server2016以上任意版本。
用户环境:
加域和不加域。
加域单独提权。
win10以上都能用。
打印机支持:
只支持网络打印机。别的没要求。
原理:每个网络打印机编写一个对应的连接bat脚本。然后通过py写的程序调用安装。中间添加驱动,域控识别等等。
环境准备工作:
1.windows10 :作为你的开发环境,用来写bat、py和测试。最好是和你的用户用的版本是一样的。
2.server2016以上:需要一个http服务器作为存放配置文件的地方。当然如果你们的it架构稳定,完全不需要。
实操
1.环境搭建
windwos10
把你的windows10,安装一下py和你用的编译器:python3.13 、 pycharm2024。这是我用的版本,其他版本也可以的,具体安装不会的话百度就行了,pycharm试用就可以。
域控账户
如果是域控环境需要一个管理员权限的域账户,且密码不能修改。放心不会明文体现。没域控就不管。
ps:你敢信一个企业一半用户加域一半用户不加。
windwos server2016
配置一个http服务器,配置好后在浏览器里访问服务器ip 能打开就可以了(如图),我这边给的是everyone的只读权限,如果你们有要求就弄复杂点。操作自行百度哈。
2.程序配置
此脚本经过我大量测试,基本适配常规版本windwos主机哈
目录文件配置:
(这个文件夹和目录名字可以自定义不过其他的都要改哈)
1.在你的d盘建一个printserver的文件夹。
2.用pycharm在printserver目录下建一个项目(不会就百度)
3.建一个p3.py文件(主程序)
4.建一个start.py文件(启动程序)
5.在printserver文件夹下新建几个目录和文件:
print 目录:用来放置我们的脚本文件
drive目录:用来存放驱动
printer_config.json:程序的配置文件
version.json:用来版本控制的配置文件。
1.ioc :图标文件。就是logo,没有的话不管。
1.reg:注册表文件
runas提权程序(域控用户必备):下面是链接。具体怎么用百度也有的。Runas with password and encrypted administrator credentials by RunAsSpcRunas with password and encrypted administrator credentials by RunAsSpc
ok配置完目录之后,开始编写脚本和代码,为了节省时间照抄就行了。
打印机连接脚本
脚本文件需要放在print目录下,可以根据你那边办公室新建目录便于区分,但是只能用英文目录不能用中文。
例:我有一个hr办公室的hp7720打印机,那么你可以在print下面建一个HRoffice目录然后在HRoffice目录下建一个HRoffice-HP7720.bat文件。你有多个部门或者办公室都可以新建只要确保在print目录下。
脚本命名:脚本的名字必须有打印机型号,最好带有和你的驱动一样的名字
那么这个HRoffice-HP7720.BAT文件就作为我们链接hr办公室的打印机脚本。代码照抄就行了。
驱动安装
使用脚本前,需要先安装驱动,网上下载一个HP7720的单驱动程序,我都是用的驱动天空的。
不能用官方的那种安装的。HP OfficeJet Pro 7720 驱动下载 - 驱动天空
安装好驱动之后会在c盘的路径产生一个文件夹,这个文件夹就是HP7720驱动的目录。C:\Windows\System32\DriverStore\FileRepository\hpygid24_v4.inf_amd64_437296df88ae7886
ps:如果你有不同型号的打印机,先安装驱动,然后就在这个C:\Windows\System32\DriverStore\FileRepository\
目录下按照修改日期找到最近安装的就是对应的驱动目录了。
ps:目录是用来判断是否安装驱动的,如果不需要忽略就行。
驱动程序名称
我们安装好驱动后先在本机上链接一下目标打印机,使用刚才安装的驱动,就可以知道驱动的名称。
打印机属性-高级-新驱动程序。然后找到对应的厂商和打印机就可以。
那么我们这里的驱动名称就是:HP OfficeJet Pro 7720 series PCL-3
ps:因为有的驱动有很多名称,搞错了就不行了。
打印机连接BAT编写
只需要修改以下的内容见就可以适配别的:
打印的命名-驱动-和ip。
:: 定义全局变量
set "打印机名[0]=HR办公室HP7720" (安装后的打印机名)
set "PRINTER_NAME=HR办公室HP7720" (安装后的打印机名)
set "printerIP=10.1.10.1" (打印机的ip地址)
set "portName=%printerIP%"
set "DRIVER_NAME=HP OfficeJet Pro 7720 series PCL-3" (填写驱动程序的名称)
判断驱动安装代码:
如果不需要直接把这个删掉就行了。
:: 定义驱动目录和INF文件名
set "DIR_PATH=C:\Windows\System32\DriverStore\FileRepository\hpygid24_v4.inf_amd64_437296df88ae7886" (驱动的目录)
echo 正在验证驱动目录是否存在...
if not exist "%DIR_PATH%\" (
echo 错误:驱动目录 "%DIR_PATH%" 未找到
exit /b 4
)
全部代码。右键以管理员身份运行,过一会就自动安装了,可以测试下。
ps:本来单程序是有一个窗口显示的,但是结合程序我就直接取消掉了。
@echo off
:: 切换代码页为 UTF-8
chcp 65001 >nul
echo 更改命令行窗口宽高
MODE con: COLS=75 LINES=45
rem 自动提权以管理员方式运行
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
if '%errorlevel%' EQU '5' (
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
pushd "%CD%"
CD /D "%~dp0"
echo =------------打印机正在安装中,请稍等------------=
echo -------------------------------------
echo 当前时间:%DATE%
echo -------------------------------------
color 0a
setlocal enabledelayedexpansion
:: 定义全局变量
set "打印机名[0]=HR办公室HP7720"
set "PRINTER_NAME=HR办公室HP7720"
set "printerIP=10.1.10.1"
set "portName=%printerIP%"
set "DRIVER_NAME=HP OfficeJet Pro 7720 series PCL-3"
echo 检测同名打印机并删除
for /L %%i in (0,1,0) do (
set "当前打印机名=!打印机名[%%i]!"
echo 正在检查打印机: !当前打印机名!
wmic printer where "Name='!当前打印机名!'" delete /nointeractive >nul 2>&1
if !errorlevel! equ 0 (
echo 已删除打印机: !当前打印机名!
) else (
echo 未找到打印机: !当前打印机名!
)
)
echo 检查网络打印机 %printerIP% 是否在线...
ping -n 1 %printerIP% >nul || (
echo 错误: 打印机 %printerIP% 无法访问,请检查网络连接
exit /b 1
)
echo 验证TCP端口9100是否开放...
powershell -WindowStyle Hidden -Command "Test-NetConnection %printerIP% -Port 9100 | Out-Null" || (
echo 错误: 打印机端口9100未响应,请检查防火墙或打印机设置
exit /b 2
)
echo 创建打印机TCP/IP端口:%portName%
cscript //nologo C:\Windows\System32\Printing_Admin_Scripts\zh-CN\prnport.vbs -a -r %portName% -h %printerIP% -o raw -n 9100
echo 检查端口是否创建成功...
cscript //nologo C:\Windows\System32\Printing_Admin_Scripts\zh-CN\prnport.vbs -l | find /i "%portName%" || (
echo 错误: 端口 %portName% 创建失败
exit /b 3
)
echo 停止并重启打印服务...
net stop spooler /y >nul && net start spooler >nul
:: 定义驱动目录和INF文件名
set "DIR_PATH=C:\Windows\System32\DriverStore\FileRepository\hpygid24_v4.inf_amd64_437296df88ae7886"
echo 正在验证驱动目录是否存在...
if not exist "%DIR_PATH%\" (
echo 错误:驱动目录 "%DIR_PATH%" 未找到
exit /b 4
)
echo 正在安装打印机 "%PRINTER_NAME%"...
rundll32 printui.dll,PrintUIEntry /if /b "%PRINTER_NAME%" /m "%DRIVER_NAME%" /r "%portName%" /q || (
echo 错误:打印机安装失败 (错误码 %errorlevel%)
exit /b %errorlevel%
)
:: ========================= 完成提示 =========================
echo.
echo =====================================================================
echo 恭喜,打印机安装完成!
echo =====================================================================
timeout /t 3 /nobreak >nul
exit /b 0
ok那么一个打印机脚本弄好了之后,可以根据你的情况把你需要做成脚本的网络打印机全部按照这个方式加进来。存在print目录里。
打印机安装程序
脚本写完了之后,我们还会遇到问题,
1.打印机太多了不可能每次都给用户发一个脚本运行吧。
2.用户加域没有管理员权限不能装驱动
3.个别未开智同事不会用
4.脚本用户乱发,导致乱连,当然只存在我们这里办公网和打印机ip都通的情况。
一眼难尽哈,问题还很多没办法。
ok回归正题,那么我们打印机脚本配置完并且能成功安装之后,那么我们接着编写程序。
先把依赖配置好。
程序依赖
drive文件夹
把你下载的驱动文件安装包都存放都这里,有哪些驱动就放哪些可以参考我的。
命名:以品牌型号命名。需要标准一点,不然识别会有问题。
print文件夹
以你的办公室或者组织架构创建目录,然后在对应办公室或者部门目录下放置你的打印机脚本。可以放多个。目录只需要在print下面就好了几级目录无所谓,不能有中文,不然识别不了。
http服务器
将http服务器共享的文件夹存放三个文件:
两个压缩文件都是上面print文件和drive文件夹打包好的。另一个version.json文件也是复制的。可以在服务器上存一个副本,修改完之后打包然后放到http服务器的目录里就可以了。
print.zip文件
print.zip文件是由print文件夹(你存放脚本的目录)+printer_config.json程序配置文件一起压缩的,更新print文件夹,同步也需要更新printer_config.json程序配置文件。printer_config.json文件放在print的根目录就好了。如果你更新了你的打印机列表需要在服务器上同步更新print文件夹中的脚本和目录以及printer_config.json程序配置文件
dirve.zip文件
dirve.zip文件是由drive文件夹压缩来的,如果你有驱动需要更新,替换掉你需要更新的驱动然后重新打包放到http服务器上,修改服务器上的version.json文件中的"versiondrive"版本信息,客户端再次打开时就会自动更新。
version.json文件
用来控制版本的文件。就是确保用户的配置文件和服务器上的配置文件一致。
分别在服务器的文件的根目录和打印机程序的根目录各放置一份。
文件说明内容如下:(注意需要全英文输入)
"server_url": "http://10.10.10.1/version.json", #http服务器配置文件的地址
"versionprint": "1.2.2", #print文件夹和printer_config.json文件的版本
"versiondrive": "1.1.1", #drive文件中的驱动版本,
以上两个版本信息如果和客户端不一致,客户端运行时会自动下载服务器上的对应文件夹名称的压缩包,然后解压替换客户端中的文件,文件替换后会更新客户端的版本信息。
"versionexe": "1.1.1" #控制程序的版本,如果程序版本和服务器上不一致,会提示更新程序无法打开。
完整的内容就是下面这个,修改你服务器的ip就可以了。
{
"server_url": "http://10.10.10.1/version.json",
"versionprint": "1.2.2",
"versiondrive": "1.1.1",
"versionexe": "1.1.1"
}
printer_config.json文件
printer_config.json文件是主程序生成列表和目录的文件,如果print目录发生修改那printer_config.json文件也需要修改。
printer_config.json文件只需要放在程序的根目录就好了,程序会自己调用。
解释:
"name": "行政楼", #代表的是楼栋,也可以是其他的在程序左边的一级目录
"num": "2楼", #代表的是楼层在程序左边的二级目录。
"name": "IT办公室", #代表的是办公室,是程序左边的三级目录。
"drive_path": "print/C1/C1-B1/F2-itoffice", #是对应办公室的打印机脚本所在目录,比如说我it办公室的打印机连接脚本在print/itoffice。那么将这个目录修改成print/itoffice即可,注意要全英文目录。
"ip_ranges": ["0.0.0.0"], #是允许访问的ip地址段,这里我写0.0.0.0就是允许所有ip访问,也就是所有ip都可以获取到这个打印机列表并连接,如果你想限制,修改为你想允许的ip就可以了。例如:"ip_ranges": ["10.1.10.1/24",“10.1.11.2/32”], 这个意思就是运行10.1所有地址段的ip1访问和运行11.2这一个ip访问,懂点网络的应该明白了。
"printers": [
{"name": "it办公室-Epson-5890"}
] #打印机列表,其中的"name"需要和你的脚本命名一样才能调用。比如我的脚本名:it办公室.bat,那么你的"name"需要改为:it办公室。.bat是写在程序里的不需要写。这个办公室有几个脚本你就添加几个就好了。
{
"campuses": [
{
"name": "行政楼",
"buildings": [
{
"num": "2楼",
"offices": [
{
"name": "IT办公室",
"drive_path": "print/C1/C1-B1/F2-itoffice",
"ip_ranges": ["0.0.0.0"],
"printers": [
{"name": "it办公室-Epson-5890"}
]
}
]
},
{
"num": "3楼",
"offices": [
{
"name": "HR办公室",
"drive_path": "print/C1/C1-B3/F2-HR",
"ip_ranges": ["0.0.0.0"],
"printers": [
{"name": "C1B3-F2HR办公室-HP7720"}
]
}
]
}
]
},
逻辑有点复杂,每一个{}都是一个层级,最大的是一级目录,然后二级三级,自己琢磨一下就懂了。需要添加的话也只需要在对应的层级里复制一份一样的。注意{}如果不是结尾需要添加英文逗号,。给你们截图解释一下吧,每个目录数量都是无所谓的,只需要找到规律然后复制黏贴修改就好了。
p3.py 主程序
(运行这个程序需要把你的pycharm以管理员方式运行)
1.需要先把import引用的库全部都安装掉,怎么安装自行百度。pycharm里面项目也可以安装。
2.如果不需要提权操作这个程序就可以直接用了。可以直接跳到打包操作。
3.需要先配置好http服务器才能打开不然会报错。
4.printer_config.json文件需要配置好放在程序根目录下,不然打开是空白的
5.version.json要文件需要配置好,不然会提缺少配置文件,版本可以先和服务器保持一致。
6.print和drive文件夹也要配置好,不然运行不了。
代码直接复制进去运行就可以了,如果有报错那么找对应文件的关联,这只是第一步。看到这里要很有耐心了,我写的也有点晦涩难懂。
注意:如果你的电脑不是域控环境,那么下面就不需要看啦,下面是域控内提权的操作。你现在只需要百度怎么把py文件打包成程序(管理员),然后把程序和依赖放在同一个文件夹下。再百度一下,怎么把文件夹打包成安装包就可以拉。当然我的打包方式也会在后面写上的。
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import socket
import subprocess
from ipaddress import ip_network, ip_address
import sys
from pathlib import Path
import os
import re
import difflib
from difflib import SequenceMatcher
import winreg
import zipfile
import requests
import threading
import shutil
import sys
from pathlib import Path
from tkinter import *
from tkinter import ttk, messagebox
import zipfile
import tkinter as tk
from tkinter import ttk, messagebox
class VersionChecker:
def __init__(self, master):
self.master = master
self.master.title("正在进行版本更新")
self.master.geometry("500x250")
master.iconbitmap('1.ico')
# 初始化UI组件
self.create_widgets()
self.local_version = {}
self.server_version = {}
# 开始异步检查
threading.Thread(target=self.start_check, daemon=True).start()
def create_widgets(self):
"""创建界面组件"""
self.progress = ttk.Progressbar(self.master, length=400, mode="determinate")
self.progress.pack(pady=20)
self.status_label = Label(self.master, text="正在进行安全检查( ̄▽ ̄)~*", font=('幼圆', 10))
self.status_label.pack(pady=5)
self.detail_label = Label(self.master, text="", font=('幼圆', 9), fg="gray")
self.detail_label.pack()
def update_ui(self, progress, message, detail=""):
"""更新界面状态"""
self.progress["value"] = progress
self.status_label.config(text=message)
self.detail_label.config(text=detail)
self.master.update_idletasks()
def load_local_version(self):
"""加载本地版本信息"""
try:
with open("version.json", "r", encoding="utf-8") as f:
self.local_version = json.load(f)
return True
except Exception as e:
messagebox.showerror("错误", f"版本配置文件不存在,请联系IT\n{str(e)}")
return False
def fetch_server_version(self):
"""获取服务器版本信息"""
try:
response = requests.get(
self.local_version["server_url"],
timeout=10,
headers={"User-Agent": "VersionChecker/1.0"}
)
response.encoding = "utf-8"
self.server_version = response.json()
return True
except Exception as e:
messagebox.showerror("错误", f"服务器无法连接,请联系IT\n{str(e)}")
return False
def download_and_update(self, module_name):
"""下载并更新模块"""
try:
# 删除旧目录
target_dir = Path(module_name)
if target_dir.exists():
shutil.rmtree(target_dir)
# 下载ZIP文件
zip_url = f"{self.local_version['server_url'].rsplit('/', 1)[0]}/{module_name}.zip"
self.update_ui(self.progress["value"], f"董酱正在搬运文件○( ̄﹏ ̄)○{module_name}")
response = requests.get(zip_url, stream=True)
total_size = int(response.headers.get("content-length", 0))
zip_path = Path(f"{module_name}.zip")
with open(zip_path, "wb") as f:
downloaded = 0
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
downloaded += len(chunk)
progress = self.progress["value"] + (downloaded / total_size) * 20
self.update_ui(progress, "", f"已下载 {downloaded / 1024:.1f}KB / {total_size / 1024:.1f}KB")
# 解压ZIP文件
if module_name == "print":
self.unzip_print_zip()
else:
self.unzip_drive_zip()
# 更新本地版本号
self.local_version[f"version{module_name}"] = self.server_version[f"version{module_name}"]
with open("version.json", "w", encoding="utf-8") as f:
json.dump(self.local_version, f, ensure_ascii=False, indent=2)
zip_path.unlink()
return True
except Exception as e:
messagebox.showerror("错误", f"{module_name}更新失败:\n{str(e)}")
return False
def unzip_print_zip(self):
# 检查文件是否存在
zip_path = "print.zip"
if not os.path.exists(zip_path):
messagebox.showerror("错误", f"未找到文件: {zip_path}")
return False
# 验证文件大小
file_size = os.path.getsize(zip_path)
if file_size < 100: # 假设zip文件至少100字节
messagebox.showerror("错误", "文件大小异常,可能不完整")
return False
# 创建解压窗口
root = tk.Tk()
root.title("解压进度")
root.geometry("400x150")
root.iconbitmap('1.ico')
progress_label = tk.Label(root, text="正在验证print.zip...", font=("Arial", 12))
progress_label.pack(pady=10)
progress = ttk.Progressbar(root, orient="horizontal", length=300, mode="determinate")
progress.pack(pady=10)
status_label = tk.Label(root, text="准备解压...", font=("Arial", 10))
status_label.pack(pady=5)
root.update()
try:
# 首先验证zip文件有效性
progress['value'] = 10
status_label.config(text="压缩文件校验")
root.update()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# 测试zip文件是否损坏
test_result = zip_ref.testzip()
if test_result is not None:
messagebox.showerror("错误", f"文件损坏: {test_result}")
return False
# 获取文件列表
file_list = zip_ref.namelist()
total_files = len(file_list)
# 解压每个文件
for i, file in enumerate(file_list):
try:
# 更新进度
progress['value'] = 10 + (i + 1) / total_files * 90
status_label.config(text=f"解压: {file[:30]}...")
root.update()
# 解压文件(支持中文路径)
zip_ref.extract(file)
except Exception as e:
messagebox.showwarning("警告", f"解压文件 {file} 时出错: {str(e)}")
continue
status_label.config(text="解压完成!")
# messagebox.showinfo("成功", "解压完成")
return True
except zipfile.BadZipFile:
messagebox.showerror("错误", "钱钱文件有问题!!!(ŎдŎ;)")
return False
except Exception as e:
messagebox.showerror("错误", f"解压过程中出错: {str(e)}")
return False
finally:
try:
root.destroy()
except:
pass
def unzip_drive_zip(self):
# 检查文件是否存在
zip_path = "drive.zip"
if not os.path.exists(zip_path):
messagebox.showerror("错误", f"未找到文件: {zip_path}")
return False
# 验证文件大小
file_size = os.path.getsize(zip_path)
if file_size < 100: # 假设zip文件至少100字节
messagebox.showerror("错误", "文件大小异常,可能不完整")
return False
# 创建解压窗口
root = tk.Tk()
root.title("解压进度")
root.geometry("400x150")
root.iconbitmap('1.ico')
progress_label = tk.Label(root, text="正在验证print.zip...", font=("Arial", 12))
progress_label.pack(pady=10)
progress = ttk.Progressbar(root, orient="horizontal", length=300, mode="determinate")
progress.pack(pady=10)
status_label = tk.Label(root, text="准备解压...", font=("Arial", 10))
status_label.pack(pady=5)
root.update()
try:
# 首先验证zip文件有效性
progress['value'] = 10
status_label.config(text="文件检验中")
root.update()
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# 测试zip文件是否损坏
test_result = zip_ref.testzip()
if test_result is not None:
messagebox.showerror("错误", f"文件损坏: {test_result}")
return False
# 获取文件列表
file_list = zip_ref.namelist()
total_files = len(file_list)
# 解压每个文件
for i, file in enumerate(file_list):
try:
# 更新进度
progress['value'] = 10 + (i + 1) / total_files * 90
status_label.config(text=f"解压: {file[:30]}...")
root.update()
# 解压文件(支持中文路径)
zip_ref.extract(file)
except Exception as e:
messagebox.showwarning("警告", f"解压文件 {file} 时出错: {str(e)}")
continue
status_label.config(text="解压完成!")
# messagebox.showinfo("成功", "本地文件解压完成")
return True
except zipfile.BadZipFile:
messagebox.showerror("错误", "文件异常,请检查")
return False
except Exception as e:
messagebox.showerror("错误", f"解压过程中出错: {str(e)}")
return False
finally:
try:
root.destroy()
except:
pass
def check_version(self, module):
"""版本检查通用逻辑"""
local_ver = self.local_version.get(f"version{module}", "0.0.0")
server_ver = self.server_version.get(f"version{module}", "0.0.0")
return local_ver == server_ver, (local_ver, server_ver)
def start_check(self):
"""主检查流程"""
# 步骤1:加载本地版本信息
if not self.load_local_version():
self.master.after(0, self.master.destroy)
return
self.update_ui(20, "正在连接服务器...")
# 步骤2:获取服务器版本
if not self.fetch_server_version():
self.master.after(0, self.master.destroy)
return
self.update_ui(40, "开始版本比对...")
# 步骤3:检查EXE版本
exe_ok, (local_exe, server_exe) = self.check_version("exe")
if not exe_ok:
self.master.after(0, lambda: messagebox.showwarning(
"错误",
f"当前程序版本 {local_exe}\n服务器版本 {server_exe}\n请下载最新版本程序"
))
self.master.after(0, self.master.destroy)
sys.exit()
# 步骤4:检查Print版本
print_ok, versions = self.check_version("print")
if not print_ok:
if not self.download_and_update("print"):
self.master.after(0, self.master.destroy)
return
# 步骤5:检查Drive版本
drive_ok, versions = self.check_version("drive")
if not drive_ok:
if not self.download_and_update("drive"):
self.master.after(0, self.master.destroy)
return
# 步骤6:检查全部通过
self.update_ui(100, "校验通过,正在启动程序")
self.master.after(2000, self.launch_main_app)
def launch_main_app(self):
"""启动主程序"""
self.master.destroy()
main_root = Tk()
PrinterInstallerApp(main_root)
main_root.mainloop()
class PrinterInstallerApp:
def __init__(self, master):
self.master = master
master.title("打印机自助安装系统")
master.geometry("800x600")
os.environ['PYTHONIOENCODING'] = 'utf-8'
master.iconbitmap('1.ico')
# 初始化配置
self.CONFIG_FILE = "printer_config.json"
self.IP_RECORD_FILE = "ip_history.log"
self.current_ip = self.get_valid_ip()
self.office_data = {} # 存储办公室信息 {显示名称: 配置数据}
self.IP_RECORD_FILE = "ip_history.txt"
self.NAME_FILE = "name.txt"
self.FILE_ENCODING = 'utf-8'
self.fix_file_encoding()
self.current_hostname = self._get_filtered_hostname()
# 初始化界面
self.setup_ui()
self.load_config()
self.init_directory()
self.check_first_run()
def setup_ui(self):
"""构建界面布局"""
# 主容器
main_frame = tk.Frame(self.master,bg="#2b2bc4")
main_frame.pack(fill=tk.BOTH, expand=True)
# 左侧目录面板(1/3宽度)
left_panel = tk.Frame(main_frame, width=300, bg="#2b2bc4")
left_panel.pack(side=tk.LEFT, fill=tk.BOTH)
# 右侧操作面板
right_panel = tk.Frame(main_frame,bg="#1e59bf")
right_panel.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH)
# 添加顶部按钮容器(占满右侧面板的宽度)
top_btn_frame = tk.Frame(right_panel)
top_btn_frame.pack(fill=tk.X)
# 构建目录树
self.create_treeview(left_panel)
# 打印机选择界面
self.create_printer_ui(right_panel)
style = ttk.Style()
style.theme_use("clam")
style.configure("Accent.TButton",
foreground="black",
font=("幼圆", 13),
background="#FFC0CB",
borderwidth=2, # 边框粗细
bordercolor="gray", # 默认边框颜色
relief="solid" # 显示边框
)
# 状态映射(动态效果)
style.map("Accent.TButton",
# 边框颜色逻辑
bordercolor=[
('selected', 'blue'), # 选中时边框变红
('!selected', 'gray') # 未选中时恢复灰色
],
# 字体颜色逻辑
foreground=[
('pressed', 'green'), # 按下时字体变红
('active', '#C71585'), # 悬停保持黑字
('!disabled', '#C71585') # 常态黑字
]
)
self.create_status_bar()
os.path.join(os.getcwd(), "printer_install.log")
def create_treeview(self, parent):
"""创建现代风格三级目录树(视觉优化版)"""
style = ttk.Style()
style.theme_use("clam")
# ====== 配色方案 ======
COLOR_SCHEME = {
"primary": "#2b2bc4", # 主色调(靛蓝)
"secondary": "#818CF8", # 辅助色(浅蓝)
"accent": "#c7dffc", # 强调色(翡翠绿)
"background": "#c5dff9", # 背景色(雪白)
"text_primary": "#1E293B", # 主要文字
"text_secondary": "#64748B" # 次要文字
}
# ====== 全局样式重置 ======
style.configure("TFrame", background=COLOR_SCHEME["background"])
# ====== 标题容器优化 ======
header_frame = ttk.Frame(parent, style="Header.TFrame")
header_frame.pack(fill=tk.X, pady=(0, 0), padx=9, ipady=0)
style.configure(
"Header.TFrame",
background=COLOR_SCHEME["primary"],
borderwidth=0,
relief="flat",
padding=(20, 10) # 增加内边距
)
# 主标题样式(带轻微投影)
style.configure(
"MainTitle.TLabel",
font=("Microsoft YaHei", 18, "bold"),
foreground="white",
anchor="center",
padding=(0, 5),
background=COLOR_SCHEME["primary"],
relief="flat"
)
# 副标题样式(带图标装饰)
style.configure(
"SubTitle.TLabel",
font=("Microsoft YaHei", 12),
foreground="#E0E7FF", # 浅蓝色
padding=(0, 0, 0, 5), # 右侧留空给装饰线
background=COLOR_SCHEME["primary"]
)
# 标题布局(增加视觉装饰)
ttk.Label(
header_frame,
text="这里可以改logo文字",
style="MainTitle.TLabel"
).pack(side=tk.TOP, fill=tk.X, pady=(10, 2))
ttk.Label(
header_frame,
text="▍选择你所在的办公室",
style="SubTitle.TLabel"
).pack(side=tk.TOP, fill=tk.X)
search_frame = ttk.Frame(header_frame)
search_frame.pack(pady=(2, 2), fill=tk.X, padx=2)
# 搜索输入框
style.configure("Search.TEntry",
fieldbackground="white",
foreground=COLOR_SCHEME["text_primary"],
borderwidth=1,
bordercolor=COLOR_SCHEME["primary"],
relief="flat",
padding=(3, 2))
# 新增提示文字专属样式(灰色文字)
style.map("Search.TEntry",
foreground=[('active', COLOR_SCHEME["text_primary"]),
('!active', COLOR_SCHEME["text_secondary"])])
self.search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame,
textvariable=self.search_var,
style="Search.TEntry")
# 添加提示文字功能
def set_placeholder(event=None):
if not self.search_var.get() and not search_entry.focus_get():
self.search_var.set("输入办公室名称搜索")
search_entry.configure(foreground=COLOR_SCHEME["text_secondary"]) # 灰色提示文字
def clear_placeholder(event):
if self.search_var.get() == "输入办公室名称搜索":
self.search_var.set("")
search_entry.configure(foreground=COLOR_SCHEME["text_primary"]) # 恢复正常文字颜色
# 绑定事件
search_entry.bind("<FocusIn>", clear_placeholder)
search_entry.bind("<FocusOut>", lambda e: set_placeholder())
search_entry.after(100, set_placeholder) # 初始显示提示文字
search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=2)
search_entry.bind("<KeyRelease>", self.search_tree)
# 实时搜索
# 清空按钮
style.configure("Clear.TButton",
background=COLOR_SCHEME["accent"],
foreground="#539aef",
borderwidth=0,
padding=(1, 1),
width=6, # 固定宽度8个字符宽
font=('Microsoft YaHei', 12) # 新增字体参数(调大字号)
)
clear_btn = ttk.Button(
search_frame,
text="清空",
style="Clear.TButton",
command=lambda: self.search_var.set("")
)
clear_btn.pack(side=tk.RIGHT, padx=(2, 2))
# ====== 树形结构深度美化 ======
style.configure(
"Modern.Treeview",
font=('Microsoft YaHei', 12),
foreground=COLOR_SCHEME["text_primary"],
rowheight=25, # 增加行高
fieldbackground="white",
bordercolor="#6495ED",
borderwidth=2,
padding=(5, 8), # 元素内边距
relief="flat"
)
style.map("Modern.Treeview",
background=[('selected', COLOR_SCHEME["secondary"])],
foreground=[('selected', 'white')],
relief=[('hover', 'groove')]) # 悬停效果
# ====== 卡片式布局容器 ======
tree_container = ttk.Frame(parent, style="TreeContainer.TFrame")
tree_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 20))
style.configure(
"TreeContainer.TFrame",
background="white",
borderwidth=2,
relief="groove",
bordercolor="#91c0ff",
padding=10
)
# ====== 高级滚动条设计 ======
style.configure(
"Modern.Vertical.TScrollbar",
troughcolor="white",
background=COLOR_SCHEME["secondary"],
bordercolor="#6495ED",
arrowsize=12,
gripcount=0,
relief="flat"
)
style.map("Modern.Vertical.TScrollbar",
background=[('active', COLOR_SCHEME["primary"])])
# ====== 树形实现 ======
self.tree = ttk.Treeview(
tree_container,
show="tree",
selectmode="browse",
style="Modern.Treeview"
)
vsb = ttk.Scrollbar(
tree_container,
orient="vertical",
command=self.tree.yview,
style="Modern.Vertical.TScrollbar"
)
self.tree.configure(yscrollcommand=vsb.set)
# ====== 弹性布局 ======
self.tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
tree_container.grid_columnconfigure(0, weight=1)
tree_container.grid_rowconfigure(0, weight=1)
return self.tree
def search_tree(self, event=None):
"""树形目录搜索功能"""
query = self.search_var.get().lower()
# 清除原有高亮
for item in self.tree.get_children():
self._reset_item_style(item)
if not query:
return
# 递归搜索所有节点
def check_children(parent):
for child in self.tree.get_children(parent):
text = self.tree.item(child, "text").lower()
if query in text:
self._highlight_match(child)
# 展开父级节点
self.tree.item(self.tree.parent(child), open=True)
# 递归展开所有祖先节点
p = self.tree.parent(child)
while p:
self.tree.item(p, open=True)
p = self.tree.parent(p)
check_children(child) # 递归检查子节点
check_children("")
def _highlight_match(self, item):
"""高亮匹配项"""
self.tree.item(item, tags=("match",))
self.tree.tag_configure("match", background="#E6E6FA", foreground="#6495ED")
def _reset_item_style(self, item):
"""重置节点样式"""
self.tree.item(item, tags=())
for child in self.tree.get_children(item):
self._reset_item_style(child)
def create_printer_ui(self, parent):
"""构建打印机选择界面(美化版)"""
# 主容器配置
parent.configure(bg="#f8fafc") # 设置整体背景色
# ================= 标题区域 =================
title_frame = tk.Frame(parent, bg="#4579e0", height=90)
title_frame.pack(fill=tk.X, pady=(0, 0))
title_frame.pack_propagate(False)
# 标题文字容器
title_content = tk.Frame(
title_frame,
bg="#4579e0",
height=60 # 显式设置像素高度:ml-citation{ref="2" data="citationList"}
)
title_content.pack(
pady=12, # 垂直间距缩减40%:ml-citation{ref="4" data="citationList"}
fill=tk.X,
expand=True
)
title_content.pack_propagate(False) # 阻断子组件影响:ml-citation{ref="7" data="citationList"}
# 修改标签布局为居中
ttk.Label(
title_content,
text="📠 打印机自助安装程序",
font=("Microsoft YaHei", 20, "bold"),
background="#4579e0",
foreground="#e0f2fe",
padding=(0, 0, 0, 10)
).pack(anchor="center") # 修改关键点
# 优雅的下划线装饰
sep_line = tk.Canvas(title_content, bg="#93c5fd", height=3, highlightthickness=0)
sep_line.create_line(0, 2, 60, 2, fill="#93c5fd", width=3)
sep_line.pack(fill=tk.X, pady=(5, 0))
# ================= 提示信息 =================
hint_frame = tk.Frame(parent, bg="#f8fafc", height=10)
hint_frame.pack(pady=(0, 2))
# 使用多个Label实现灵活排版
tk.Label(
hint_frame,
text="首次安装目标型号的打印机请先点击:",
font=('Microsoft YaHei', 11),
fg="#a086f4",
bg="#f8fafc",
).pack(side=tk.LEFT)
# 可点击的驱动安装链接
self.driver_link = tk.Label(
hint_frame,
text="安装驱动",
font=('Microsoft YaHei', 12, "underline"),
fg="#a086f4",
bg="#f8fafc",
cursor="hand2"
)
self.driver_link.pack(side=tk.LEFT, padx=5)
self.driver_link.bind("<Button-1>", lambda e: self.drive())
# ================= 打印机列表区域 =================
list_frame = tk.Frame(parent, bg="#f8fafc")
list_frame.pack(fill=tk.BOTH, expand=True, padx=35)
# 列表标题(现代卡片式设计)
header_frame = tk.Frame(list_frame, bg="#e0e7ff", bd=0, relief="flat", height=6)
header_frame.pack(fill=tk.X, pady=(5, 5))
tk.Label(
header_frame,
text="▎选择你需要连接的打印机",
font=('Microsoft YaHei', 13,),
bg="#e0e7ff",
fg="#8883f7",
padx=10
).pack(side=tk.LEFT)
# 列表框容器(带阴影效果)
list_container = tk.Frame(list_frame, bg="#ffffff",
relief="groove",
borderwidth=2,
highlightbackground="#e2e8f0")
list_container.pack(fill=tk.BOTH, expand=True)
# 创建滚动条样式
style = ttk.Style()
style.theme_use('clam') # 使用支持颜色定制的主题
# 配置滚动条轨道和滑块颜色
style.configure("Vertical.TScrollbar",
troughcolor="#f1f5f9", # 滑道背景色
arrowsize=14, # 箭头大小
arrowcolor="#64748b", # 箭头颜色
bordercolor="#e2e8f0", # 边框颜色
darkcolor="#e2e8f0", # 暗部颜色
lightcolor="#e2e8f0", # 亮部颜色
gripcount=0) # 隐藏默认的滑块纹理
# 修改滑块颜色(需要单独配置)
style.map("Vertical.TScrollbar",
background=[('active', '#6495ed'), # 激活状态颜色
('!active', '#818cf8')]) # 非激活状态颜色
# 创建滚动条
scrollbar = ttk.Scrollbar(
list_container,
style="Vertical.TScrollbar" # 应用自定义样式
)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 优化后的列表框
self.printer_list = tk.Listbox(
list_container,
font=('Microsoft YaHei', 14),
selectbackground="#6366f1",
selectforeground="white",
activestyle="none",
borderwidth=0,
yscrollcommand=scrollbar.set,
bg="#ffffff",
highlightthickness=0
)
self.printer_list.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
scrollbar.config(command=self.printer_list.yview)
btn_frame = tk.Frame(parent, bg="#f8fafc")
btn_frame.pack(pady=25, fill=tk.X)
# 按钮样式配置
btn_style = ttk.Style()
btn_style.theme_use('clam')
# ===== Driver样式保留 =====
btn_style.configure("Driver.TButton",
font=('幼圆', 13, "bold"),
foreground="#1e3a8a",
background="#bfdbfe",
padding=10)
btn_style.map("Driver.TButton",
foreground=[('active', '#1e3a8a')],
background=[('active', '#93c5fd')])
# ===== 开始安装按钮使用Driver样式 =====
self.install_btn = ttk.Button(
btn_frame,
text="🚀 安装打印机",
command=self.execute_installation,
style="Driver.TButton" # 关键修改点
)
self.install_btn.pack(side=tk.RIGHT, padx=(0, 80))
# ===== 安装驱动保持原样 =====
self.driver_btn = ttk.Button(
btn_frame,
text="⏏ 安装驱动",
command=self.drive,
style="Driver.TButton"
)
self.driver_btn.pack(side=tk.RIGHT, padx=(0, 120))
# ===== 移除Accent样式 =====
# 由于代码中不再使用,可以删除以下原Accent样式配置:
# btn_style.configure("Accent.TButton", ...)
# btn_style.map("Accent.TButton", ...)
# 调换到左侧并减少间距
# 删除以下悬停效果相关代码:
# def on_enter(e):
# e.widget["style"] = "Hover.TButton"
# def on_leave(e):
# e.widget["style"] = "Accent.TButton"
# btn_style.map("Accent.TButton",
# foreground=[('active', '#ffffff')],
# background=[('active', '#4f46e5')])
# for btn in [self.driver_btn, self.install_btn]:
# btn.bind("<Enter>", on_enter)
# btn.bind("<Leave>", on_leave)
# 悬停效果增强
def is_domain_joined(self):
"""检测计算机是否加入域(无需管理员权限)"""
try:
# 方法1:检查计算机全名是否包含域名
if '.' in socket.getfqdn():
return True
# 方法2:检查环境变量
if os.getenv('USERDOMAIN') and os.getenv('USERDOMAIN') != os.getenv('COMPUTERNAME'):
return True
# 方法3:检查注册表中的域信息
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters") as key:
domain = winreg.QueryValueEx(key, "Domain")[0]
if domain:
return True
except WindowsError:
pass
# 方法4:尝试解析域控制器
try:
socket.gethostbyname('_ldap._tcp.dc._msdcs.' + os.getenv('USERDOMAIN', ''))
return True
except socket.gaierror:
pass
return False
except Exception:
return False
def create_status_bar(self):
"""状态栏显示IP"""
# 状态栏框架(背景色保持粉色 #fcd4e1)
status_frame = tk.Frame(self.master, height=55, bg="#60659b")
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
# ================================================
# 1. 路径诊断按钮(保持原样式)
# ================================================
style = ttk.Style()
style.configure(
"Small.TButton",
font=("幼圆", 12),
foreground="#539aef",
background="#E6E6FA",
bordercolor="#60659b",
padding=3
)
debug_btn = ttk.Button(
status_frame,
text="路径诊断",
command=self.show_path_debug,
style="Small.TButton"
)
debug_btn.pack(side=tk.LEFT, padx=5)
# ================================================
# 2. 中间七色名字(直接使用 tk.Label)
# ================================================
names_colors = [
("技术支持:", "purple"), ("不会配交换机", "purple"),
(f"当前环境:{'域环境' if self.is_domain_joined() else '本地环境'}", "purple"),
]
# names_colors = [
# (f"当前环境:{'加入域' if is_domain_joined() else '未加域'}"),
#
# ]
# 创建两行 Frame
frame_row1 = tk.Frame(status_frame, bg="#60659b") # 第一行
frame_row2 = tk.Frame(status_frame, bg="#60659b") # 第二行
frame_row1.pack()
frame_row2.pack()
# 将前 4 个放在第一行,后 4 个放在第二行
for i, (name, color) in enumerate(names_colors):
lbl = tk.Label(
frame_row1 if i < 2 else frame_row2, # 前4个在第一行,后4个在第二行
text=name,
fg=color,
bg="#60659b",
font=("幼圆", 9),
anchor = "w"
)
lbl.pack(side=tk.LEFT, padx=2) # 保持原有格式
# ================================================
# 3. 关键修复:IP标签改用 tk.Label 并直接设置样式
# ================================================
self.ip_label = tk.Label( # 改用 tk.Label 确保颜色直接生效
status_frame,
text=f"当前IP地址: {self.current_ip}",
fg="#d6c4ff", # 直接设置字体颜色
bg="#60659b", # 背景色与框架一致
font=("微软雅黑", 12),
anchor="e" # 右对齐
)
self.ip_label.pack(side=tk.RIGHT, padx=10)
# ================================================
# 4. 占位标签微调(避免挤压IP标签)
# ================================================
spacer = tk.Label(status_frame, bg="#60659b") # 背景色与框架完全一致
spacer.pack(side=tk.LEFT, expand=True, fill=tk.X)
status_frame.pack_propagate(False) # 锁定框架高度
def show_path_debug(self):
"""显示当前路径转换详情"""
debug_info = []
for office in self.office_data.values():
raw_path = office['drive_path']
checked = "有效" if Path(raw_path).exists() else "无效"
converted = self.safe_path_convert(raw_path)
debug_info.append(
f"原始路径: {raw_path}\n"
f"状态: {checked}\n"
f"转换后: {converted}\n" + "-" * 40
)
messagebox.showinfo(
"路径诊断",
"\n".join(debug_info) if debug_info else "暂无路径信息"
)
self.ip_label = ttk.Label(
status_frame,
text=f"当前IP地址: {self.current_ip}",
background="#e0e0e0",
anchor=tk.E
)
self.ip_label.pack(side=tk.RIGHT, padx=10)
# ------------------ 核心功能方法 ------------------
def load_config(self):
"""加载并解析配置文件"""
try:
with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
# 新增路径有效性预检
path_errors = []
for campus in config['campuses']:
for building in campus['buildings']:
for office in building['offices']:
checked_path = self.safe_path_convert(office['drive_path'])
if not checked_path:
path_errors.append(office['drive_path'])
if path_errors:
messagebox.showwarning(
"配置预警",
f"路径异常,请联系IT!(。・`ω´・)\n" +
"\n".join(path_errors)
)
# 构建树形结构
for campus in config['campuses']:
campus_node = self.tree.insert("", "end", text=campus['name'])
for building in campus['buildings']:
bldg_node = self.tree.insert(
campus_node, "end",
text=f"{building['num']}"
)
for office in building['offices']:
full_name = f"{campus['name']}-{building['num']}-{office['name']}"
office_node = self.tree.insert(
bldg_node, "end",
text=office['name'],
values=[full_name]
)
# 存储办公室配置
self.office_data[full_name] = office
except Exception as e:
messagebox.showerror("配置错误", f"配置文件加载失败: {str(e)}")
self.master.destroy()
def init_directory(self):
"""初始化目录树事件"""
self.tree.bind("<<TreeviewSelect>>", self.on_select_office)
def on_select_office(self, event):
"""选择办公室事件处理"""
try:
# 1. 获取选中项
selected = self.tree.selection()
if not selected:
return
# 2. 获取项目数据
item = self.tree.item(selected[0])
if not item['values'] or len(item['values']) == 0:
item = self.tree.item(selected[0])
self.printer_list.delete(0, tk.END) # 清空列表
return
# 3. 清空打印机列表
self.printer_list.delete(0, tk.END)
# 4. 获取完整名称
full_name = item['values'][0]
if not full_name or full_name not in self.office_data:
messagebox.showwarning("警告", "无效的办公室名称")
return
# 5. 获取办公室配置
office_cfg = self.office_data.get(full_name)
if not office_cfg:
messagebox.showwarning("警告", "找不到该办公室的配置")
return
# 6. 验证IP
if not self.validate_ip(office_cfg.get('ip_ranges', [])):
messagebox.showerror("IP地址不在允许范围内",
"当前电脑IP不允许连接该办公室的打印机哦!")
self.printer_list.delete(0, tk.END)
return
# 7. 加载打印机
self.load_printers(office_cfg.get('printers', []))
except Exception as e:
messagebox.showerror("错误", f"处理选择时发生错误: {str(e)}")
self.printer_list.delete(0, tk.END)
def validate_ip(self, ip_ranges):
"""验证IP是否在指定范围内"""
current_ip = ip_address(self.current_ip)
for net in ip_ranges:
if current_ip in ip_network(net, strict=False):
return True
return False
def load_printers(self, printers):
"""加载打印机列表"""
self.printer_list.delete(0, tk.END)
for printer in printers:
self.printer_list.insert(tk.END, printer['name'])
def safe_path_convert(self, raw_path):
"""安全处理中文路径(适用于Windows特殊场景)"""
try:
# 优先尝试直接使用原生路径
p = Path(raw_path)
if p.exists():
return str(p.resolve())
except OSError:
pass
# 处理UTF-8字节编码(针对部分Windows特殊环境)
try:
win_path = raw_path.encode('utf-8').decode(sys.getfilesystemencoding())
p = Path(win_path)
if p.exists():
return str(p.resolve())
except UnicodeError:
pass
# 双重验证路径是否存在
if os.path.exists(raw_path):
return raw_path
return None
def drive(self):
"""执行安装操作(集成日志自动清理)"""
if not self.printer_list.curselection():
messagebox.showwarning("选择错误", "请先选择要安装的打印机驱动")
return
# 获取当前选择的打印机名称
printer_name = self.printer_list.get(self.printer_list.curselection())
# 预处理打印机名称(去中文/符号/统一小写)
def clean_name(name):
# 移除所有中文字符 [\u4e00-\u9fff]
name = re.sub(r'[\u4e00-\u9fff]', '', name)
# 保留字母数字并转为小写
return ''.join(filter(str.isalnum, name)).lower()
clean_printer = clean_name(printer_name)
# 获取驱动目录下所有可执行文件
dr_dir = "drive"
try:
all_files = os.listdir(dr_dir)
except FileNotFoundError:
messagebox.showerror("错误", f"驱动目录不存在: {dr_dir}")
return
# 过滤并处理EXE文件名
candidates = []
for f in all_files:
if f.lower().endswith('.exe'):
# 清理文件名(去除中文和符号)
base_name = os.path.splitext(f)[0] # 去掉扩展名
cleaned = clean_name(base_name)
candidates.append((f, cleaned)) # 保留原始文件名
if not candidates:
messagebox.showerror("错误", f"驱动目录中没有可执行文件")
return
# 模糊匹配算法
best_match = None
highest_ratio = 0
for orig_name, cleaned_name in candidates:
# 计算相似度
ratio = SequenceMatcher(
None,
clean_printer,
cleaned_name
).ratio()
# 如果驱动名称包含完整打印机型号数字,提高优先级
printer_digits = ''.join(filter(str.isdigit, clean_printer))
driver_digits = ''.join(filter(str.isdigit, cleaned_name))
if printer_digits and (printer_digits in driver_digits):
ratio += 0.3 # 加权
if ratio > highest_ratio:
highest_ratio = ratio
best_match = orig_name
# 设置匹配阈值(可调整)
if highest_ratio < 0.5: # 低于40%相似度视为不匹配
messagebox.showerror("错误",
f"找不到合适驱动,最高匹配度:{highest_ratio:.1%}\n"
f"打印机特征:{clean_printer}\n"
f"候选驱动:{[c[0] for c in candidates]}")
return
# 构建完整路径
exe_path = os.path.join(dr_dir, best_match)
# 执行安装程序
try:
subprocess.run(exe_path, check=True, shell=True)
messagebox.showinfo("成功",
f"烫工帮你找来了最好的,拿着吧\( ̄︶ ̄)/{best_match}\n"
f"匹配相似度:{highest_ratio:.1%}")
except subprocess.CalledProcessError as e:
messagebox.showerror("错误", f"安装失败: {str(e)}")
def execute_installation(self):
"""执行安装操作(集成日志自动清理)"""
if not self.printer_list.curselection():
messagebox.showwarning("不对不对,选错了", "请先选择要安装的打印机")
return
# 获取当前选择的办公室和打印机
selected_office = self.tree.item(self.tree.selection()[0])['values'][0]
printer_name = self.printer_list.get(self.printer_list.curselection())
office_cfg = self.office_data[selected_office]
# 构建批处理路径
bat_path = os.path.join(
office_cfg['drive_path'],
f"{printer_name}.bat"
)
if not os.path.exists(bat_path):
messagebox.showerror("路径错误", f"安装脚本不存在:\n{bat_path}")
return
try:
bat_path_str = str(bat_path)
# === 关键修改1:转换中文路径为Windows短路径 ===
try:
import win32api
bat_path_str = win32api.GetShortPathName(bat_path_str)
except ImportError:
messagebox.showwarning("警告", "未安装 pywin32 库,长路径中文可能无法处理")
except Exception as e:
print(f"短路径转换失败: {e}")
# 自动创建隐藏根窗口(如果不存在)
if not tk._default_root:
root = tk.Tk()
root.withdraw()
else:
root = tk._default_root
# 创建日志窗口(增加字体设置)
log_window = tk.Toplevel()
log_window.title("安装日志实时输出")
log_window.iconbitmap('1.ico')
log_text = tk.Text(
log_window,
wrap=tk.WORD,
height=20,
width=70,
font=("Microsoft YaHei", 10) # 新增简体字体支持
)
log_text.pack(padx=10, pady=10)
# 生成临时日志路径
log_path = os.path.join(os.getcwd(), "printer_install.log")
# --- 初始化变量 ---
last_file_size = 0
is_running = True
# === 关键修改2:修正命令编码和路径处理 ===
wrapped_cmd = (
f'chcp 65001 > nul && ' # 强制UTF-8编码
f'cmd /c ""{bat_path_str}"" ' # 双引号包裹路径
f'> "{log_path}" 2>&1'
)
# --- 日志监控函数 ---
def update_log_display():
nonlocal last_file_size
if not is_running or not log_window.winfo_exists():
return
try:
# === 使用GBK编码读取日志 ===
with open(log_path, "r", encoding='utf-8', errors='ignore') as f:
f.seek(last_file_size)
new_content = f.read()
if new_content:
log_text.insert(tk.END, new_content)
log_text.see(tk.END)
last_file_size = f.tell()
except FileNotFoundError:
log_text.insert(tk.END, "等待日志生成...\n") # 简体确认
except Exception as e:
log_text.insert(tk.END, f"日志错误: {str(e)}\n") # 简体确认
log_window.after(300, update_log_display)
# --- 启动日志监控 ---
log_window.after(100, update_log_display)
# --- 异步执行安装 ---
from threading import Thread
def run_installation():
exit_code = os.system(wrapped_cmd)
log_window.after(0, lambda: show_result(exit_code)) # 使用log_window调度
def show_result(exit_code):
nonlocal is_running
is_running = False
# 关闭日志窗口
if log_window.winfo_exists():
log_window.destroy()
# 清理日志文件
self._cleanup_logs(log_path)
# 显示结果提示(简体确认)
if exit_code == 0:
messagebox.showinfo(
"打印机给装上了(★>U<★) ",
"打印机装好啦,稍后就可以用咯(✪ω✪)",
parent=root
)
elif exit_code == 1:
messagebox.showerror(
"安装失败",
"打印机无法访问,请检查网络",
parent=root
)
elif exit_code == 2:
messagebox.showerror(
"安装失败",
"打印机端口9100未响应,请检查防火墙或打印机设置",
parent=root
)
elif exit_code == 3:
messagebox.showerror(
"安装失败",
"打印机端口创建失败,请联系IT处理",
parent=root
)
elif exit_code == 4:
messagebox.showerror(
"董酱!来装下驱动Ψ( ̄∀ ̄)Ψ",
"打印机驱动未安装,请先点击“安装驱动”按钮哦",
parent=root
)
else:
error_msg = f"安装失败 (错误码: {exit_code})\n请检查文件: {bat_path}"
messagebox.showerror(
"安装错误",
error_msg,
parent=root
)
# 窗口关闭保护
def on_closing():
nonlocal is_running
is_running = False
if log_window.winfo_exists():
log_window.destroy()
self._cleanup_logs(log_path)
log_window.protocol("WM_DELETE_WINDOW", on_closing)
# 启动线程
Thread(target=run_installation, daemon=True).start()
except Exception as e:
messagebox.showerror("意外错误", f"执行失败: {str(e)}") # 简体确认
def _cleanup_logs(self, log_path):
"""日志清理方法"""
try:
if os.path.exists(log_path):
os.remove(log_path)
except Exception as e:
print(f"日志清理失败: {str(e)}")
def _cleanup_logs(self, log_path):
"""安全清理日志文件(新增方法)"""
try:
if os.path.exists(log_path):
os.remove(log_path)
print(f"已清理日志文件: {log_path}")
except Exception as e:
print(f"日志清理失败: {str(e)}")
# ------------------ 系统功能 ------------------
def get_valid_ip(self):
"""获取有效IP地址"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except Exception as e:
messagebox.showerror("网络错误", f"无法获取IP地址: {str(e)}")
return "0.0.0.0"
# def check_first_run(self):
# """首次运行检查"""
# if not os.path.exists(self.IP_RECORD_FILE):
# messagebox.showwarning(
# "首次运行",
# "必须初始化安装程序!\n请执行 anzhuang.py 进行环境准备"
# )
# self.record_ip()
def record_ip(self):
"""记录IP到历史文件"""
try:
with open(self.IP_RECORD_FILE, 'a') as f:
f.write(f"{self.current_ip}\n")
except Exception as e:
messagebox.showerror("写入错误", f"IP记录失败: {str(e)}")
def _get_filtered_hostname(self):
"""获取并过滤非法字符的主机名"""
try:
raw_name = socket.gethostname()
# 过滤所有操作系统非法字符
illegal_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|', '\0']
return ''.join(c for c in raw_name if c not in illegal_chars)[:255] or "UNKNOWN_HOST"
except Exception as e:
messagebox.showerror("系统错误", f"获取主机名失败: {str(e)}")
return "UNKNOWN_HOST"
def fix_file_encoding(self):
"""修复现有文件编码问题"""
for file_path in [self.IP_RECORD_FILE, self.NAME_FILE]:
if os.path.exists(file_path):
try:
# 尝试用UTF-8读取文件
with open(file_path, 'r', encoding=self.FILE_ENCODING) as f:
content = f.read()
# 重新规范写入
with open(file_path, 'w', encoding=self.FILE_ENCODING) as f:
f.write(content.strip() + '\n') # 标准化换行
except UnicodeDecodeError:
# 重建损坏文件
os.remove(file_path)
# messagebox.showinfo("文件修复", f"已重建文件: {os.path.basename(file_path)}")
except Exception as e:
messagebox.showerror("文件错误", f"修复失败: {str(e)}")
def check_first_run(self):
"""增强版首次运行检查"""
if not all(os.path.exists(f) for f in [self.IP_RECORD_FILE, self.NAME_FILE]):
messagebox.showwarning(
"温馨提示",
"第一次安装打印机,请先点击“安装驱动”按钮,再安装打印机哦!"
)
self.record_ip()
self._record_hostname()
def record_ip(self):
"""带编码控制的记录方法"""
try:
with open(self.IP_RECORD_FILE, 'a', encoding=self.FILE_ENCODING) as f:
f.write(f"{self.current_hostname}\n")
except Exception as e:
messagebox.showerror("写入错误", f"IP记录失败: {str(e)}")
def _record_hostname(self):
"""带编码控制的名称记录"""
try:
with open(self.NAME_FILE, 'a', encoding=self.FILE_ENCODING) as f:
f.write(self.current_hostname)
except Exception as e:
messagebox.showerror("写入错误", f"名称记录失败: {str(e)}")
if __name__ == "__main__":
root = Tk()
VersionChecker(root)
root.mainloop()
# if __name__ == "__main__":
# root = tk.Tk()
#
# # 配置界面样式
# style = ttk.Style()
# style.configure("Treeview", font=('微软雅黑', 10), rowheight=25)
# style.map("Treeview", background=[('selected', '#BBDEFB')])
# style.configure("Accent.TButton", font=('微软雅黑', 12),
# foreground="white", background="#2196F3")
#
# app = PrinterInstallerApp(root)
# root.mainloop()
# font_candidates = [
# 'Microsoft YaHei', # 微软雅黑
# 'SimHei', # 黑体
# 'FangSong', # 仿宋
# 'KaiTi', # 楷体
# 'Arial Unicode MS' # 跨平台字体
# ]
#
# # 自动选择可用字体
# for font in font_candidates:
# try:
# root.option_add("*Font", (font, 10))
# test_label = ttk.Label(root, text="中文测试")
# test_label.destroy()
# break
# except tk.TclError:
# continue
start.py启动程序
start.py是因为我这边有加域或者不加域的电脑,所有额外加了一个判断的。这个脚本不能直接运行需要先把提权软件和其他的配置好之后才能运行。
如果不加域就直接打开我们打包的p3.exe文件,如果加域那么就用我开头提要的runas软件。
这里就是判断的拉,如果加域那么就通过我们的提权软件来打开程序。如果不加域就直接启动我们打包的p3.exe文件(虽然还没打包,先往下看)如果你的文件名有修改直接改括号里的就好了。如图
直接复制代码
import os
import sys
import socket
import subprocess
import ctypes
import winreg
def is_domain_joined():
"""检测计算机是否加入域(无需管理员权限)"""
try:
if '.' in socket.getfqdn():
return True
if os.getenv('USERDOMAIN') and os.getenv('USERDOMAIN') != os.getenv('COMPUTERNAME'):
return True
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters") as key:
domain = winreg.QueryValueEx(key, "Domain")[0]
if domain: return True
except WindowsError:
pass
try:
socket.gethostbyname('_ldap._tcp.dc._msdcs.' + os.getenv('USERDOMAIN', ''))
return True
except socket.gaierror:
pass
return False
except Exception:
return False
def run_program(exe_name):
"""更健壮的启动方式"""
try:
script_dir = os.path.dirname(sys.executable if getattr(sys, 'frozen', False) else __file__)
exe_path = os.path.normpath(os.path.join(script_dir, exe_name))
if os.path.exists(exe_path):
subprocess.Popen(exe_path, shell=True)
else:
print(f"错误:未找到可执行文件 {exe_path}")
except Exception as e:
print(f"启动失败: {e}")
def main():
if is_domain_joined():
print("检测到计算机已加入域")
run_program("runas.exe")
else:
print("检测到计算机未加域")
ctypes.windll.shell32.ShellExecuteW(None, "runas",
os.path.abspath("p3.exe"),
None, None, 1)
if __name__ == "__main__":
main()
py打包成exe操作
好了能看到这里也是厉害,上面逻辑有点乱可以不看解释,不许要搞懂逻辑的只要会用就好了,
准备工作:确保这些依赖都配置好,然后两个脚本都写好。p3.py脚本能够打开界面,并运行就好了。
p3.py打包:p3打包有点特殊,需要添加管理员权限打包。
命令:pyinstaller --onefile --noconsole --uac-admin p3.py
start.py打包:直接打包就好了。
命令:pyinstaller --onefile --noconsole start.py
p3.py打包操作:
1.打开我们的pycharm,在下方运行选择控制台选择--终端
2.cd至你的程序所在的目录,也就是p3.py的目录。
3.进到目录依次输入p3.py的打包命令回车就好了。因为我这里目录不一样就不截图了。
4.打包完成后会在当前目录生成一个dist目录,打包好的程序就在里面啦。
5.把打包好的程序拷贝至上层目录,也就是程序根目录。
p3提权操作
p3打包完成之后,就可以直接运行了,但是在域控环境下呢,还需要输入管理员账户和密码,就用我们的下载的runas程序解决吧。
下载runas,在准备工作就有的,里面有链接直接下载。下载之后拷贝到程序根目录。如图这两个文件。
注意:我代码里写的是runas.exe,,这里可以改代码也可以吧runasspc.exe文件重命名成runas.exe.
使用步骤:
1.打开runasspacadmin.exe文件,然后按照这个截图操作。域账户要管理员哈。(截图是早期的会少一点文件不用管)
2.证书生成之后,运行ruanasspc.exe就会直接用管理员账户打开p3文件啦。不需要输密码,这样就好了。
start.py打包操作:
1.和我们的p3.py打包操作一样,也是进入pycharm控制台,然后输入打包命令。
2.拷贝到根目录就好了。过多就不赘述了,记得改一下执行的文件名保持一致哈,就是runas.exe那个。
解释:start打包完成后作为我们的主程序入口,加域则会调用ruansspc打开p3,不加域就直接打开p3。现在你运行start程序,不管你加不加域都不需要输密码了。
将所有程序打包成安装包
好了,程序我们卸完了,但是你不能直接把文件夹或者压缩文件给用户用吧,那么还需要将我们这些文件打包成一个安装程序。用户安装之后会创建快捷方式到桌面和我们常用的程序一样。
1.在c盘建一个文件夹,比如printstart。
2.把我们程序全部拷贝进去
3.重新生成证书(由于提权程序证书目录必须固定,所以只能在指定的文件夹打包)当前目录。
4.下载 inno setup 打包程序,自己百度下载。
5.打开inno setup 编辑器。(算了我还是截图吧,虽然很麻烦)
6.创建一个新的脚本文件
7.这些程序信息自己编吧,然后下一步。
8.按照截图要求,填完下一步。
9.执行文件选择start.exe。
10.选择添加文件,把目录下的文件全选,全部添加。
11.添加根文件夹。
12.默认下一步,不需要修改。
13.按照我的勾选下一步
14.许可文件没有就下一步
15.只勾选管理员安装模式
16.注册表可以不打,但是我打了。直接下一步
17.这里选一下你需要保存的路径就好了
18.点击下一步,就可以直接编译了,运行之后就会在你指定的目录生成一个安装包
普通用户用这个安装包就直接安装就好了
具体怎么用你自己研究写一个sop给用户就ok了。