python + tkinter实现的静态文件服务器

功能概述:基于python,tkinter实现的静态文件服务器,将exe文件丢到需要作为静态网站访问的目录中,双击运行即可在这个目录下提供web服务,
支持修改服务端口,支持缩放到托盘,实时日志查看等。代码均有注释方便初学者学习python图形界面和基本功能。

功能实现分为3个类,直接上源码

运行源码之前,需要先安装 pypiwin32 , 命令:pip install pypiwin32  #用于实现缩放到托盘

 

 

1.图形面板GUI 也是main类,使用tkinter,代码都又注释,就不赘述了

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from tkinter import *
import tkinter.messagebox as messagebox
import time
import HttpServer
import SysTrayIcon

window_width=480
window_height=500
LOG_LINE_NUM = 0
web_port=9000
this_http_server=None
systrayIconObj = None
init_window=None

class main_gui():
    '''主界面类'''
    def __init__(self,this_window, toTrayIconFun):
        self.this_window = this_window
        self.toTrayIconFun=toTrayIconFun

        '''界面类容  '''
        # 说明书
        self.init_desc_label = Label(self.this_window, fg="red", text="将此文件放到任意目录,①打开软件,②设置端口号,③点击启动 即可启动web服务。")
        self.init_desc_label.grid(row=0, column=0, rowspan=1, columnspan=6)
        # 端口文字
        self.init_port_label = Label(self.this_window, text="WEB端口:")
        self.init_port_label.grid(row=1, column=0)
        # 端口输入文本框
        self.init_port_Text = Entry(self.this_window, textvariable=StringVar(value=web_port))  # 原始数据录入框
        self.init_port_Text.grid(row=1, column=1, rowspan=1, columnspan=1)

        # 启动 按钮
        self.start_server_button = Button(self.this_window, text="启动WEB服务", bg="lightblue", width=10,
                                          command=self.start_server)  # 调用内部方法  加()为直接调用
        self.start_server_button.grid(row=1, column=2, rowspan=1, columnspan=1)
        # 隐藏到图标
        self.hide_server_button = Button(self.this_window, text="托盘隐藏", bg="lightblue", width=10,
                                         command=self.toTrayIconFun)  # 调用内部方法  加()为直接调用
        self.hide_server_button.grid(row=1, column=3)

        # 日志
        self.log_data_label = Label(self.this_window, text="日志类容:")
        self.log_data_label.grid(row=3, column=0, rowspan=1, columnspan=6)

        # 日志
        self.log_data_Text = Text(self.this_window, width=66, height=29)  # 日志框
        self.log_data_Text.grid(row=4, column=0, columnspan=6)

        # 清空日志
        self.start_server_button = Button(self.this_window, text="清空日志", bg="lightblue", width=10,
                                          command=self.clear_log)  # 调用内部方法  加()为直接调用
        self.start_server_button.grid(row=5, column=0, columnspan=6)


    #隐藏到托盘
    def hide_to_stock(self):
        pass

    # 日志动态打印
    def write_log_to_Text(self, logmsg,err=None):
        print(f" 日志:{logmsg}",err)
        current_time = self.get_current_time()
        logmsg_in = str(current_time) + " " + str(logmsg) + "\n"  # 换行
        self.log_data_Text.insert("insert", logmsg_in)
        if err !=None:
            self.log_data_Text.insert("insert", f"{err}" + "\n")
        self.log_data_Text.update()

    # 清空日志
    def clear_log(self):
        self.log_data_Text.delete(1.0, END)
        self.log_data_Text.update()

    def start_server(self):
        iport  = self.init_port_Text.get().strip().replace("\n", "")
        if not iport.isdigit() or (int(iport)<=0 or 65000<=int(iport)):
            messagebox.showerror(title = '端口错误',message='端口必须是数字,且只能填写1-65000之中的数字。')
            return
        web_port=int(iport)
        print(f" 新端口:{web_port}")
        global this_http_server
        if this_http_server :
            this_http_server.close_server()

        this_http_server = HttpServer.HttpServer(web_port,self.write_log_to_Text);

    #获取当前时间
    def get_current_time(self):
        current_time = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
        return current_time

def exit(s=None,_sysTrayIcon=None):
    global init_window
    init_window.destroy()# 退出界面loop,退出进程
    print('exit...')

def Hidden_window(icon='icon.ico', hover_text="静态文件服务器"):
        '''隐藏窗口至托盘区,调用SysTrayIcon的重要函数'''
        # 托盘图标右键菜单, 格式: ('name', None, callback),下面也是二级菜单的例子
        # 24行有自动添加‘退出’,不需要的可删除
        menu_options = ()
        init_window.withdraw()  # 隐藏tk窗口
        global systrayIconObj
        if not systrayIconObj:
            systrayIconObj = SysTrayIcon.SysTrayIcon(
                icon,  # 图标
                hover_text,  # 光标停留显示文字
                menu_options,  # 右键菜单
                on_quit=exit,  # 退出调用
                tk_window=init_window,  # Tk窗口
            )
        #创建托盘图标
        systrayIconObj.activation()


def gui_start():
    global init_window
    init_window = Tk()              #实例化出一个父窗口
    init_window.resizable(False, False)  # 窗口不可调整大小
    #init_window.iconbitmap("icon.ico") #icod不方便打包,所以不单独设置icon了,
    init_window.title("静态文件服务器")  # 窗口名
    init_window["bg"] = "#F0F0F0"  # 窗口背景色,其他背景色见:blog.csdn.net/chl0000/article/details/7657887
    init_window.attributes("-alpha", 1)  # 虚化,值越小虚化程度越高
    main_gui(init_window, Hidden_window)
    # 设置窗口初始大小和位置 window_width window_height为窗口大小,+10 +10 定义窗口弹出时的默认展示位置
    init_window.geometry('%dx%d+%d+%d' % (window_width, window_height, 300, 300))
    #绑定缩放托盘事件
    init_window.bind("<Unmap>",  lambda event: Hidden_window() if init_window.state() == 'iconic' else False)  # 窗口最小化判断,可以说是调用最重要的一步
    init_window.protocol('WM_DELETE_WINDOW', exit)  # 点击Tk窗口关闭时直接调用s.exit,不使用默认关闭
    init_window.update()  # 刷新窗口显示
    init_window.mainloop()          #父窗口进入事件循环,可以理解为保持窗口运行,否则界面不展示


if __name__ == "__main__":
    gui_start()

 

2.实现缩放到托盘的工具类

import win32api, win32con, win32gui_struct, win32gui
import os, tkinter as tk


class SysTrayIcon(object):
    '''SysTrayIcon类用于显示任务栏图标'''
    QUIT = 'QUIT'
    SPECIAL_ACTIONS = [QUIT]
    FIRST_ID = 5320

    def __init__(s, icon, hover_text, menu_options, on_quit, tk_window=None, default_menu_index=None,
                 window_class_name=None):
        '''
        icon         需要显示的图标文件路径
        hover_text   鼠标停留在图标上方时显示的文字
        menu_options 右键菜单,格式: (('a', None, callback), ('b', None, (('b1', None, callback),)))
        on_quit      传递退出函数,在执行退出时一并运行
        tk_window    传递Tk窗口,s.root,用于单击图标显示窗口
        default_menu_index 不显示的右键菜单序号
        window_class_name  窗口类名
        '''
        s.icon = icon
        s.hover_text = hover_text
        s.on_quit = on_quit
        s.root = tk_window

        menu_options = menu_options + (('退出', None, s.QUIT),)
        s._next_action_id = s.FIRST_ID
        s.menu_actions_by_id = set()
        s.menu_options = s._add_ids_to_menu_options(list(menu_options))
        s.menu_actions_by_id = dict(s.menu_actions_by_id)
        del s._next_action_id

        s.default_menu_index = (default_menu_index or 0)
        s.window_class_name = window_class_name or "SysTrayIconPy"

        message_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): s.restart,
                       win32con.WM_DESTROY: s.destroy,
                       win32con.WM_COMMAND: s.command,
                       win32con.WM_USER + 20: s.notify, }
        # 注册窗口类。
        wc = win32gui.WNDCLASS()
        wc.hInstance = win32gui.GetModuleHandle(None)
        wc.lpszClassName = s.window_class_name
        wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW;
        wc.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
        wc.hbrBackground = win32con.COLOR_WINDOW
        wc.lpfnWndProc = message_map  # 也可以指定wndproc.
        s.classAtom = win32gui.RegisterClass(wc)

    def activation(s):
        '''激活任务栏图标,不用每次都重新创建新的托盘图标'''
        hinst = win32gui.GetModuleHandle(None)  # 创建窗口。
        style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
        s.hwnd = win32gui.CreateWindow(s.classAtom,
                                       s.window_class_name,
                                       style,
                                       0, 0,
                                       win32con.CW_USEDEFAULT,
                                       win32con.CW_USEDEFAULT,
                                       0, 0, hinst, None)
        win32gui.UpdateWindow(s.hwnd)
        s.notify_id = None
        s.refresh(title='软件已后台!', msg='点击重新打开', time=500)

        win32gui.PumpMessages()

    def refresh(s, title='', msg='', time=500):
        '''刷新托盘图标
           title 标题
           msg   内容,为空的话就不显示提示
           time  提示显示时间'''
        hinst = win32gui.GetModuleHandle(None)
        if os.path.isfile(s.icon):
            icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
            hicon = win32gui.LoadImage(hinst, s.icon, win32con.IMAGE_ICON,
                                       0, 0, icon_flags)
        else:  # 找不到图标文件 - 使用默认值
            hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)

        if s.notify_id:
            message = win32gui.NIM_MODIFY
        else:
            message = win32gui.NIM_ADD

        s.notify_id = (s.hwnd, 0,  # 句柄、托盘图标ID
                       win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP | win32gui.NIF_INFO,
                       # 托盘图标可以使用的功能的标识
                       win32con.WM_USER + 20, hicon, s.hover_text,  # 回调消息ID、托盘图标句柄、图标字符串
                       msg, time, title,  # 提示内容、提示显示时间、提示标题
                       win32gui.NIIF_INFO  # 提示用到的图标
                       )
        win32gui.Shell_NotifyIcon(message, s.notify_id)

    def show_menu(s):
        '''显示右键菜单'''
        menu = win32gui.CreatePopupMenu()
        s.create_menu(menu, s.menu_options)

        pos = win32gui.GetCursorPos()
        win32gui.SetForegroundWindow(s.hwnd)
        win32gui.TrackPopupMenu(menu,
                                win32con.TPM_LEFTALIGN,
                                pos[0],
                                pos[1],
                                0,
                                s.hwnd,
                                None)
        win32gui.PostMessage(s.hwnd, win32con.WM_NULL, 0, 0)

    def _add_ids_to_menu_options(s, menu_options):
        result = []
        for menu_option in menu_options:
            option_text, option_icon, option_action = menu_option
            if callable(option_action) or option_action in s.SPECIAL_ACTIONS:
                s.menu_actions_by_id.add((s._next_action_id, option_action))
                result.append(menu_option + (s._next_action_id,))
            else:
                result.append((option_text,
                               option_icon,
                               s._add_ids_to_menu_options(option_action),
                               s._next_action_id))
            s._next_action_id += 1
        return result

    def restart(s, hwnd, msg, wparam, lparam):
        s.refresh()

    def destroy(s, hwnd=None, msg=None, wparam=None, lparam=None, exit=1):
        nid = (s.hwnd, 0)
        win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid)
        win32gui.PostQuitMessage(0)  # 终止应用程序。
        if exit and s.on_quit:
            s.on_quit()  # 需要传递自身过去时用 s.on_quit(s)
        else:
            s.root.deiconify()  # 显示tk窗口

    def notify(s, hwnd, msg, wparam, lparam):
        '''鼠标事件'''
        if lparam == win32con.WM_LBUTTONDBLCLK:  # 双击左键
            pass
        elif lparam == win32con.WM_RBUTTONUP:  # 右键弹起
            s.show_menu()
        elif lparam == win32con.WM_LBUTTONUP:  # 左键弹起
            s.destroy(exit=0)
        return True
        """
        可能的鼠标事件:
          WM_MOUSEMOVE      #光标经过图标
          WM_LBUTTONDOWN    #左键按下
          WM_LBUTTONUP      #左键弹起
          WM_LBUTTONDBLCLK  #双击左键
          WM_RBUTTONDOWN    #右键按下
          WM_RBUTTONUP      #右键弹起
          WM_RBUTTONDBLCLK  #双击右键
          WM_MBUTTONDOWN    #滚轮按下
          WM_MBUTTONUP      #滚轮弹起
          WM_MBUTTONDBLCLK  #双击滚轮
        """

    def create_menu(s, menu, menu_options):
        for option_text, option_icon, option_action, option_id in menu_options[::-1]:
            if option_icon:
                option_icon = s.prep_menu_icon(option_icon)

            if option_id in s.menu_actions_by_id:
                item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text,
                                                                hbmpItem=option_icon,
                                                                wID=option_id)
                win32gui.InsertMenuItem(menu, 0, 1, item)
            else:
                submenu = win32gui.CreatePopupMenu()
                s.create_menu(submenu, option_action)
                item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text,
                                                                hbmpItem=option_icon,
                                                                hSubMenu=submenu)
                win32gui.InsertMenuItem(menu, 0, 1, item)

    def prep_menu_icon(s, icon):
        # 加载图标。
        ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
        ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
        hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE)

        hdcBitmap = win32gui.CreateCompatibleDC(0)
        hdcScreen = win32gui.GetDC(0)
        hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y)
        hbmOld = win32gui.SelectObject(hdcBitmap, hbm)
        brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU)
        win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush)
        win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL)
        win32gui.SelectObject(hdcBitmap, hbmOld)
        win32gui.DeleteDC(hdcBitmap)

        return hbm

    def command(s, hwnd, msg, wparam, lparam):
        id = win32gui.LOWORD(wparam)
        s.execute_menu_option(id)

    def execute_menu_option(s, id):
        menu_action = s.menu_actions_by_id[id]
        if menu_action == s.QUIT:
            win32gui.DestroyWindow(s.hwnd)
        else:
            menu_action(s)

3.socket网络服务类

import socket
import os
import threading
import  sys

class HttpServer(object):
    '''提供web socket提供web服务 '''
    __isRun = True ##允许外界主动关闭提供关闭函数
    __mainThread=None

    def __init__(self,port,logfun=None):
        self.__port =port
        if logfun != None :
            self.__logfun =logfun
        else :
            self.__logfun = self.print_log

        self.__logfun(f'{self.__port}端口创建新的服务器')

        mysocketThread = threading.Thread(target=self.start_server,args=("",port))
        mysocketThread.setDaemon(True)
        mysocketThread.start()
        self.__mainThread = mysocketThread

    def print_log(self, logmsg,err=None):
        print(f"{logmsg}",err)

    def hander_reuqest(self,newsocket , ip_port):
        try:
            recv_data = newsocket.recv(4096)
            self.__logfun(f'新增请求:{recv_data}')
            recv_data = recv_data.decode("utf-8")
            if recv_data == "":
                newsocket.close()
                return

            recv_data = recv_data.split(" ", maxsplit=2)
            url = recv_data[1]
            url = url.replace("..","")
            while url.startswith("/"):
                url=url[1:]

            if url=="":
                url = "index.html"

            self.__logfun(f'请求预处理链接:{url}')

            header = "HTTP/1.1 200 ok\r\n" + "server:conanServer \r\n" + "\r\n"
            try:
                with open(url, "rb") as file:
                    file_data = file.read()
            except Exception as err:
                self.__logfun(f'链接对应静态文件获取失败:{url}',err)
                header = "HTTP/1.1 400 Not Found File\r\n" + "server:conanServer \r\n" + "\r\n"
                file_data ='<!DOCTYPE html><html><head><meta charset="utf-8"><title>出错啦</title></head><body>访问的文件不存在</body></html> '.encode("utf-8")

            content = header.encode("utf-8") + file_data
            newsocket.send(content)
        except Exception as err:
            self.__logfun(f'请求的链接处理失败:{url}',err)
            header = "HTTP/1.1 400 other error\r\n" + "server:conanServer \r\n" + "\r\n"
            file_data = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>出错啦</title></head><body>未知错误</body></html>'.encode("utf-8")
            content = header.encode("utf-8") + file_data
            newsocket.send(content)
        finally:
             newsocket.close()

    def close_server(self):
        self.__isRun=False
        self.__logfun(f'{self.__port}端口,执行停止此端口的web服务')
        self.__soket_server.close()


    def start_server (self,ip="",port="8888"):
        try:
            self.__soket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.__soket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,True)
            self.__soket_server.bind((ip, port))
            self.__soket_server.listen(128)
            self.__logfun(f'{self.__port}端口启动新服务成功,访问地址:http://127.0.0.1:{self.__port}')
            while self.__isRun:
                newsocket, ip_port = self.__soket_server.accept()
                newthread = threading.Thread(target=self.hander_reuqest, args=(newsocket, ip_port))
                newthread.setDaemon(True)
                newthread.start()
        except Exception as err:
            self.__logfun(f'{self.__port}端口服务异常',err)
        finally:
            if self.__soket_server :
                self.__soket_server.close()
            self.__logfun(f'{self.__port}端口的web服务停止成功')


    if __name__ == "__main__":
        print(sys.argv)
        port = int(sys.argv[1]) if len(sys.argv)>1 else 8880
        print(port)
        start_server("",port)

刚学python,算是笔记,有问题的可以一起交流。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值