Welcome to HACMS

HACMS网页版是基于PHP+(MySQL/SQLite)开发的开源程序,采用CodeIgniter框架、Adminlte后台模板,以会员核心系统为中心,以自身需求为出发点,采用模块分离的方式快速建立站群,具有速度快、效率高、功能强大、操作体验佳等特点,支持文章页面生成HTML静态页面、URL Rewrite、支持多数据库、多表操作与管理,包括网页版进销存、文章、MD文档等等。

HACMS桌面版是以C#、Python为开发语言,选择PostgreSQL、MySQL、SQLite为数据库,还是以自身需求为出发点,现已形成以网页源码采集器、进销存系统、键盘鼠标自动化工具、软件多开助手、单表自定义设置管理、EPUB电子书阅读器等小工具。

SQLite表名

CREATE TABLE demo_test (
    tid      INTEGER PRIMARY KEY AUTOINCREMENT DEFAULT (0),
    title    TEXT,
    notes    TEXT,
    dateline INTEGER
);

Python操作SQLite

"""
demo_test 表管理程序 (SQLite 版本)
功能:增加、编辑、删除、查看(双击查看)、分页列表
数据库表结构:
  tid       integer primary key autoincrement
  title     text not null
  notes     text not null
  dateline  integer not null default 0 (Unix timestamp)

依赖库:tkinter, sqlite3, datetime, time, urllib, PIL (Pillow)
"""

import sqlite3
import sys
import tkinter as tk
from tkinter import ttk, messagebox, Toplevel
from datetime import datetime
import time
import urllib.request
import tempfile
import os

# 尝试导入 Pillow,如果失败则禁用图标功能
try:
    from PIL import Image, ImageTk
    PILLOW_AVAILABLE = True
except ImportError:
    PILLOW_AVAILABLE = False
    print("提示:未安装 Pillow 库,无法设置窗口图标。请执行 'pip install Pillow' 安装。")

# ======================= 配置(可修改)=======================
TABLE_NAME = "demo_test"   # 表名,可随时修改
DB_FILE = "data.db"        # SQLite 数据库文件
PAGE_SIZE = 10             # 每页显示数量
ICON_URL = "http://www.hacms.com/assets/python/locate.png"  # 窗口图标 URL
# ============================================================

# ----------------------- 数据库封装 -----------------------
def show_error_and_exit(error_message):
    """弹出错误对话框并退出程序"""
    root = tk.Tk()
    root.withdraw()
    messagebox.showerror("致命错误", error_message)
    root.destroy()
    sys.exit(1)

class Database:
    """SQLite 数据库操作单例类"""
    _instance = None

    def __new__(cls):
        if not cls._instance:
            cls._instance = super().__new__(cls)
            cls._instance.conn = None
            try:
                cls._instance.connect()
                cls._instance.create_table()
            except Exception as e:
                cls._instance = None
                show_error_and_exit(f"数据库连接失败:{e}")
        return cls._instance

    def connect(self):
        """建立数据库连接"""
        self.conn = sqlite3.connect(DB_FILE)
        self.conn.row_factory = sqlite3.Row

    def create_table(self):
        """创建表(如果不存在)"""
        sql = f"""
        CREATE TABLE IF NOT EXISTS {TABLE_NAME} (
            tid INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            notes TEXT NOT NULL,
            dateline INTEGER NOT NULL DEFAULT 0
        )
        """
        self.conn.execute(sql)
        self.conn.commit()

    def close(self):
        """关闭数据库连接"""
        if self.conn:
            self.conn.close()

    def query(self, sql, args=()):
        """执行 SELECT 查询,返回字典列表"""
        cursor = self.conn.cursor()
        cursor.execute(sql, args)
        rows = cursor.fetchall()
        return [dict(row) for row in rows]

    def execute(self, sql, args=()):
        """执行 INSERT/UPDATE/DELETE,返回受影响行数,自动提交"""
        try:
            cursor = self.conn.cursor()
            cursor.execute(sql, args)
            self.conn.commit()
            return cursor.rowcount
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_total_count(self):
        """获取表中总记录数"""
        sql = f"SELECT COUNT(*) as cnt FROM {TABLE_NAME}"
        result = self.query(sql)
        return result[0]['cnt'] if result else 0

    def get_page(self, page, page_size=PAGE_SIZE):
        """分页获取数据(按 tid 降序)"""
        offset = (page - 1) * page_size
        sql = f"SELECT tid, title, notes, dateline FROM {TABLE_NAME} ORDER BY tid DESC LIMIT ? OFFSET ?"
        rows = self.query(sql, (page_size, offset))
        return rows

    def insert_record(self, title, notes, dateline):
        """新增记录,返回新插入的 tid"""
        sql = f"INSERT INTO {TABLE_NAME} (title, notes, dateline) VALUES (?, ?, ?)"
        self.execute(sql, (title, notes, dateline))
        cursor = self.conn.cursor()
        cursor.execute("SELECT last_insert_rowid() as tid")
        result = cursor.fetchone()
        return result[0] if result else None

    def update_record(self, tid, title, notes, dateline):
        """更新记录"""
        sql = f"UPDATE {TABLE_NAME} SET title=?, notes=?, dateline=? WHERE tid=?"
        return self.execute(sql, (title, notes, dateline, tid))

    def delete_record(self, tid):
        """删除记录"""
        sql = f"DELETE FROM {TABLE_NAME} WHERE tid=?"
        return self.execute(sql, (tid,))

# ----------------------- GUI 应用程序 -----------------------
class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Demo_Test 数据管理 (SQLite)")
        self.root.geometry("900x600")
        self.root.configure(bg='white')

        # 设置窗口图标
        self.set_window_icon()

        # 数据库实例
        self.db = Database()

        # 分页参数
        self.current_page = 1
        self.page_size = PAGE_SIZE
        self.total_pages = 1

        # 创建界面
        self.create_menu()
        self.create_widgets()
        self.load_data()

        # 程序退出时关闭数据库连接
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def set_window_icon(self):
        """从网络下载图片并设置为窗口图标(需 Pillow 支持)"""
        if not PILLOW_AVAILABLE:
            return
        try:
            # 下载图片
            with urllib.request.urlopen(ICON_URL, timeout=5) as response:
                img_data = response.read()
            # 保存到临时文件
            with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file:
                tmp_file.write(img_data)
                tmp_path = tmp_file.name
            # 打开并转换为 PhotoImage
            img = Image.open(tmp_path)
            icon = ImageTk.PhotoImage(img)
            self.root.iconphoto(True, icon)
            # 删除临时文件
            os.unlink(tmp_path)
        except Exception as e:
            # 图标加载失败不影响主程序,仅打印调试信息
            print(f"窗口图标加载失败:{e}")

    def create_menu(self):
        """创建菜单栏(一级菜单)"""
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)

        menubar.add_command(label="新增", command=self.add_record)
        menubar.add_command(label="编辑", command=self.edit_record)
        menubar.add_command(label="删除", command=self.delete_record)
        menubar.add_command(label="刷新", command=self.refresh)
        menubar.add_command(label="显示", command=self.view_record)

    def create_widgets(self):
        """创建所有控件(列表区域和分页控件)"""
        main_frame = tk.Frame(self.root, bg='white')
        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 列表区域 (Treeview)
        columns = ("tid", "title", "notes", "dateline")
        self.tree = ttk.Treeview(main_frame, columns=columns, show="headings")
        self.tree.heading("tid", text="序号")
        self.tree.heading("title", text="标题")
        self.tree.heading("notes", text="内容")
        self.tree.heading("dateline", text="日期")
        self.tree.column("tid", width=60, anchor="center")
        self.tree.column("title", width=200)
        self.tree.column("notes", width=400)
        self.tree.column("dateline", width=120, anchor="center")

        scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.tree.bind("<Double-1>", self.view_record)

        # 分页控件区域
        page_frame = tk.Frame(self.root, bg='white')
        page_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5)

        tk.Button(page_frame, text="首页", command=self.first_page, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(page_frame, text="上一页", command=self.prev_page, width=6).pack(side=tk.LEFT, padx=2)
        self.page_label = tk.Label(page_frame, text="第 1 页 / 共 1 页", width=15, bg='white')
        self.page_label.pack(side=tk.LEFT, padx=10)
        tk.Button(page_frame, text="下一页", command=self.next_page, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(page_frame, text="末页", command=self.last_page, width=6).pack(side=tk.LEFT, padx=2)

        tk.Label(page_frame, text="跳转页:", bg='white').pack(side=tk.LEFT, padx=(10, 2))
        self.page_spinbox = tk.Spinbox(page_frame, from_=1, to=1, width=5, command=self.jump_page)
        self.page_spinbox.pack(side=tk.LEFT, padx=2)

    def load_data(self):
        """加载当前页数据到 Treeview"""
        try:
            total = self.db.get_total_count()
            if total == 0:
                self.total_pages = 1
                self.current_page = 1
                for item in self.tree.get_children():
                    self.tree.delete(item)
                self.update_page_controls()
                return

            self.total_pages = (total + self.page_size - 1) // self.page_size
            if self.current_page > self.total_pages:
                self.current_page = self.total_pages
            if self.current_page < 1:
                self.current_page = 1

            rows = self.db.get_page(self.current_page, self.page_size)

            for item in self.tree.get_children():
                self.tree.delete(item)

            for row in rows:
                dt_str = datetime.fromtimestamp(row['dateline']).strftime("%Y-%m-%d") if row['dateline'] else ""
                self.tree.insert("", tk.END, values=(row['tid'], row['title'], row['notes'], dt_str))

            self.update_page_controls()
        except Exception as e:
            messagebox.showerror("错误", f"加载数据失败:{e}")

    def update_page_controls(self):
        """更新分页控件显示及 Spinbox 范围"""
        self.page_label.config(text=f"第 {self.current_page} 页 / 共 {self.total_pages} 页")
        self.page_spinbox.config(from_=1, to=self.total_pages)
        self.page_spinbox.delete(0, tk.END)
        self.page_spinbox.insert(0, str(self.current_page))

    def refresh(self):
        self.load_data()

    def first_page(self):
        if self.current_page != 1:
            self.current_page = 1
            self.load_data()

    def prev_page(self):
        if self.current_page > 1:
            self.current_page -= 1
            self.load_data()

    def next_page(self):
        if self.current_page < self.total_pages:
            self.current_page += 1
            self.load_data()

    def last_page(self):
        if self.current_page != self.total_pages:
            self.current_page = self.total_pages
            self.load_data()

    def jump_page(self):
        try:
            page = int(self.page_spinbox.get())
            if 1 <= page <= self.total_pages:
                self.current_page = page
                self.load_data()
            else:
                messagebox.showwarning("警告", f"页码范围 1-{self.total_pages}")
        except ValueError:
            messagebox.showerror("错误", "请输入有效数字")

    def get_selected_tid(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("提示", "请先选择一条记录")
            return None
        item = self.tree.item(selected[0])
        return item['values'][0]

    def add_record(self):
        self.open_edit_window(is_edit=False)

    def edit_record(self):
        tid = self.get_selected_tid()
        if tid is None:
            return
        sql = f"SELECT tid, title, notes, dateline FROM {TABLE_NAME} WHERE tid=?"
        rows = self.db.query(sql, (tid,))
        if not rows:
            messagebox.showerror("错误", "记录不存在")
            return
        self.open_edit_window(is_edit=True, record=rows[0])

    def open_edit_window(self, is_edit=False, record=None):
        win = Toplevel(self.root)
        win.title("编辑记录" if is_edit else "新增记录")
        win.geometry("500x350")
        win.resizable(False, False)
        win.grab_set()
        win.configure(bg='white')

        title_var = tk.StringVar()
        notes_var = tk.StringVar()
        date_var = tk.StringVar()

        if is_edit and record:
            title_var.set(record['title'])
            notes_var.set(record['notes'])
            if record['dateline']:
                date_var.set(datetime.fromtimestamp(record['dateline']).strftime("%Y-%m-%d"))
        else:
            date_var.set(datetime.now().strftime("%Y-%m-%d"))

        tk.Label(win, text="标题:", font=("", 10), bg='white').grid(row=0, column=0, padx=10, pady=10, sticky="e")
        tk.Entry(win, textvariable=title_var, width=40).grid(row=0, column=1, padx=10, pady=10, sticky="w")

        tk.Label(win, text="内容:", font=("", 10), bg='white').grid(row=1, column=0, padx=10, pady=10, sticky="ne")
        notes_text = tk.Text(win, width=40, height=10)
        notes_text.grid(row=1, column=1, padx=10, pady=10, sticky="w")
        notes_text.insert("1.0", notes_var.get())

        tk.Label(win, text="日期:", font=("", 10), bg='white').grid(row=2, column=0, padx=10, pady=10, sticky="e")
        tk.Entry(win, textvariable=date_var, width=15).grid(row=2, column=1, padx=10, pady=10, sticky="w")

        error_label = tk.Label(win, fg="red", bg='white')
        error_label.grid(row=3, column=0, columnspan=2, pady=5)

        def save():
            title = title_var.get().strip()
            notes = notes_text.get("1.0", tk.END).strip()
            date_str = date_var.get().strip()

            if not title or not notes or not date_str:
                error_label.config(text="所有字段都不能为空")
                return
            try:
                dt = datetime.strptime(date_str, "%Y-%m-%d")
                dateline = int(time.mktime(dt.timetuple()))
            except ValueError:
                error_label.config(text="日期格式错误,应为 YYYY-MM-DD")
                return

            try:
                if is_edit:
                    self.db.update_record(record['tid'], title, notes, dateline)
                    messagebox.showinfo("成功", "更新成功")
                else:
                    new_id = self.db.insert_record(title, notes, dateline)
                    messagebox.showinfo("成功", f"新增成功,新序号:{new_id}")
                win.destroy()
                self.refresh()
            except Exception as e:
                messagebox.showerror("数据库错误", f"保存失败:{e}")

        btn_frame = tk.Frame(win, bg='white')
        btn_frame.grid(row=4, column=0, columnspan=2, pady=20)
        tk.Button(btn_frame, text="保存", command=save, width=10).pack(side=tk.LEFT, padx=10)
        tk.Button(btn_frame, text="取消", command=win.destroy, width=10).pack(side=tk.LEFT, padx=10)

    def delete_record(self):
        tid = self.get_selected_tid()
        if tid is None:
            return
        if not messagebox.askyesno("确认删除", f"确定要删除序号为 {tid} 的记录吗?"):
            return
        try:
            self.db.delete_record(tid)
            messagebox.showinfo("成功", "删除成功")
            total = self.db.get_total_count()
            new_total_pages = (total + self.page_size - 1) // self.page_size
            if new_total_pages == 0:
                self.current_page = 1
            elif self.current_page > new_total_pages:
                self.current_page = new_total_pages
            self.load_data()
        except Exception as e:
            messagebox.showerror("数据库错误", f"删除失败:{e}")

    def view_record(self, event=None):
        tid = self.get_selected_tid()
        if tid is None:
            return
        sql = f"SELECT tid, title, notes, dateline FROM {TABLE_NAME} WHERE tid=?"
        rows = self.db.query(sql, (tid,))
        if not rows:
            messagebox.showerror("错误", "记录不存在")
            return
        rec = rows[0]
        dt_str = datetime.fromtimestamp(rec['dateline']).strftime("%Y-%m-%d") if rec['dateline'] else ""

        win = Toplevel(self.root)
        win.title(f"查看记录 - 序号 {rec['tid']}")
        win.geometry("500x400")
        win.grab_set()
        win.configure(bg='white')

        tk.Label(win, text=f"标题:{rec['title']}", font=("", 12, "bold"), bg='white').pack(pady=5, anchor="w", padx=10)
        tk.Label(win, text=f"日期:{dt_str}", font=("", 10), bg='white').pack(pady=5, anchor="w", padx=10)
        tk.Label(win, text="内容:", font=("", 10, "bold"), bg='white').pack(pady=5, anchor="w", padx=10)
        text_box = tk.Text(win, wrap=tk.WORD, height=15)
        text_box.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        text_box.insert("1.0", rec['notes'])
        text_box.config(state=tk.DISABLED)
        tk.Button(win, text="关闭", command=win.destroy, width=10).pack(pady=10)

    def on_closing(self):
        self.db.close()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()

帮助中心

HACMS进销存管理系统V1.0(桌面版)

HACMS进销存管理系统V1.0(桌面版)采用的是Python+MySQL开发,开发初衷仅为自己所用,主要包括采购管理、采购退货管理、销售管理、销售退货管理、库存预警、库存报表、报盈管理、报损管理、流水导出等功能 ...

使用手册

PHP

PHP is a popular general-purpose scripting language that is especially suited to web development. Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world.

PHP 手册

CodeIgniter

CodeIgniter is a powerful PHP framework with a very small footprint, built for developers who need a simple and elegant toolkit to create full-featured web applications.

CodeIgniter4用户指南