Images To ICO Converter – DarkCorners

Date: 17/12/2025

Category: Python

  • Phần mềm hỗ trợ chuyển đổi hình ảnh sang ICO hàng loạt.
  • Hỗ trợ nhiều định dạng hình ảnh khác nhau.
  1. Khởi động phần mềm.
  2. Chọn thư mục chứa toàn bộ ảnh cần chuyển đổi.
  3. Chọn thư mục xuất file ico đã chuyển đổi.
  4. Nhấn nút [Bắt Đầu].
  5. Theo dõi lịch trình xử lý.
import os
import threading
import queue
import time
from pathlib import Path
from PIL import Image
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog, messagebox

SUPPORTED_EXT = ('.png', '.jpg', '.jpeg', '.webp')
ICON_SIZE = (256, 256)

def center_window(root, width=800, height=600):
    # Lấy kích thước màn hình
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    # Tính vị trí bắt đầu trục ngang và trục đứng
    aaaxxx = (screen_width // 2) - (width // 2)
    bbbyyy = (screen_height // 2) - (height // 2) - (40)
    # Đặt geometry
    root.geometry(f"{width}x{height}+{aaaxxx}+{bbbyyy}")

class ImagesToICOConverter(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Images To ICO Converter - DarkCorners')
        self.resizable(False, False)
        center_window(self, 800, 600)
        # State
        self.input_dir = tk.StringVar(value='')
        self.output_dir = tk.StringVar(value='')
        self.total_files = tk.IntVar(value=0)
        self.converted_files = tk.IntVar(value=0)
        self.error_files = tk.IntVar(value=0)
        self.pending_files = tk.IntVar(value=0)
        # Worker control
        self.file_queue = queue.Queue()
        self.event_queue = queue.Queue()  # for GUI updates from worker
        self.worker_thread = None
        self.stop_event = threading.Event()
        self.pause_event = threading.Event()  # when set => paused
        self._build_ui()
        # poll event queue to update GUI
        self.after(200, self._process_event_queue)

    def _build_ui(self):
        # Row 1 - title label
        title_lbl = tk.Label(self, text='Images To ICO Converter - DarkCorners', font=('Segoe UI', 18, 'bold'))
        title_lbl.pack(pady=(12, 12))

        # Row 2 - input/output selectors
        row2 = tk.Frame(self, width=800, height=40)
        row2.pack_propagate(False)
        row2.pack(pady=(2, 2))
        left_frame = tk.Frame(row2, width=400, height=40)
        right_frame = tk.Frame(row2, width=400, height=40)
        left_frame.pack(side='left')
        right_frame.pack(side='right')
        # Input chooser (single row, entry + button)
        input_entry = tk.Entry(left_frame, textvariable=self.input_dir, width=51)
        input_entry.pack(side='left', padx=(10,6))
        input_btn = tk.Button(left_frame, text='Thư Mục Gốc', command=self.choose_input)
        input_btn.pack(side='left', padx=(0,10))
        # Output chooser
        output_entry = tk.Entry(right_frame, textvariable=self.output_dir, width=44)
        output_entry.pack(side='left', padx=(10,6))
        output_btn = tk.Button(right_frame, text='Thư Mục Lưu', command=self.choose_output)
        output_btn.pack(side='left', padx=(0,10))

        # Row 3 - controls and stats within LabelFrames
        row3 = tk.Frame(self, width=800, height=70)
        row3.pack_propagate(False)
        row3.pack(pady=(6,6))
        # Left labelframe: ĐIỀU KHIỂN
        ctrl_label = tk.Label(self, text='ĐIỀU KHIỂN', font=('Segoe UI', 10, 'bold'), fg='green')
        self.ctrl_lf = tk.LabelFrame(row3, labelwidget=ctrl_label, width=400, height=70)
        self.ctrl_lf.pack(side='left', padx=10)
        self.ctrl_lf.pack_propagate(False)
        btn_frame = tk.Frame(self.ctrl_lf)
        btn_frame.pack(expand=True)
        start_btn = tk.Button(btn_frame, text='Bắt Đầu', width=12, command=self.start)
        pause_btn = tk.Button(btn_frame, text='Tạm Dừng', width=12, command=self.toggle_pause)
        exit_btn = tk.Button(btn_frame, text='Thoát', width=12, command=self.exit_app)
        start_btn.pack(side='left', padx=8, pady=5)
        pause_btn.pack(side='left', padx=8, pady=5)
        exit_btn.pack(side='left', padx=8, pady=5)
        # save references to change Pause label later
        self._pause_btn = pause_btn
        # Right labelframe: THỐNG KÊ
        stats_label = tk.Label(self, text='THỐNG KÊ', font=('Segoe UI', 10, 'bold'), fg='green')
        self.stats_lf = tk.LabelFrame(row3, labelwidget=stats_label, width=400, height=70)
        self.stats_lf.pack(side='right', padx=10)
        self.stats_lf.pack_propagate(False)
        stats_frame = tk.Frame(self.stats_lf)
        stats_frame.pack(expand=True)
        # Create counters inline
        # [Tất Cả: xx] [Đã Convert: xx] [Đang Đợi: xx] [Bị Lỗi: xx]
        lbl_total = tk.Label(stats_frame, text='Tất Cả:')
        val_total = tk.Label(stats_frame, textvariable=self.total_files, font=('Segoe UI', 10, 'bold'), fg='red')
        lbl_converted = tk.Label(stats_frame, text='Đã Convert:')
        val_converted = tk.Label(stats_frame, textvariable=self.converted_files, font=('Segoe UI', 10, 'bold'), fg='green')
        lbl_pending = tk.Label(stats_frame, text='Đang Đợi:')
        val_pending = tk.Label(stats_frame, textvariable=self.pending_files, font=('Segoe UI', 10, 'bold'), fg='blue')
        lbl_error = tk.Label(stats_frame, text='Bị Lỗi:')
        val_error = tk.Label(stats_frame, textvariable=self.error_files, font=('Segoe UI', 10, 'bold'), fg='black')
        # pack inline
        for widget in [lbl_total, val_total, lbl_converted, val_converted, lbl_pending, val_pending, lbl_error, val_error]:
            widget.pack(side='left', padx=2)

        # Row 4 - logs frame
        logs_label = tk.Label(self, text='LỊCH TRÌNH HOẠT ĐỘNG', font=('Segoe UI', 10, 'bold'), fg='green')
        self.logs_lf = tk.LabelFrame(self, labelwidget=logs_label, width=780, height=380)
        self.logs_lf.pack(pady=(6,10))
        self.logs_lf.pack_propagate(False)
        # Text widget for logs with black background and white text
        logs_frame = tk.Frame(self.logs_lf)
        logs_frame.pack(fill='both', expand=True, padx=6, pady=6)
        self.log_text = tk.Text(logs_frame, bg='black', fg='white', state='disabled', wrap='word')
        self.log_text.pack(side='left', fill='both', expand=True)
        scroll = tk.Scrollbar(logs_frame, command=self.log_text.yview)
        scroll.pack(side='right', fill='y')
        self.log_text['yscrollcommand'] = scroll.set

        # window close protocol
        self.protocol('WM_DELETE_WINDOW', self.exit_app)

    # ---------------- UI actions ----------------
    def choose_input(self):
        d = filedialog.askdirectory()
        if d:
            self.input_dir.set(d)

    def choose_output(self):
        d = filedialog.askdirectory()
        if d:
            self.output_dir.set(d)

    def start(self):
        input_dir = self.input_dir.get().strip()
        output_dir = self.output_dir.get().strip()
        if not input_dir or not os.path.isdir(input_dir):
            messagebox.showwarning('Chú ý', 'Vui lòng chọn thư mục Input hợp lệ')
            return
        if not output_dir or not os.path.isdir(output_dir):
            messagebox.showwarning('Chú ý', 'Vui lòng chọn thư mục Output hợp lệ')
            return
        # If a worker is already running, ignore start
        if self.worker_thread and self.worker_thread.is_alive():
            self._log('Worker đang chạy — thao tác Start bị bỏ qua')
            return
        # gather files
        files = []
        for root, _, filenames in os.walk(input_dir):
            for fn in filenames:
                if fn.lower().endswith(SUPPORTED_EXT):
                    files.append(os.path.join(root, fn))
        # reset counters
        self.total_files.set(len(files))
        self.converted_files.set(0)
        self.error_files.set(0)
        self.pending_files.set(len(files))
        # fill queue
        while not self.file_queue.empty():
            try: self.file_queue.get_nowait()
            except queue.Empty: break
        for f in files:
            self.file_queue.put(f)
        # reset control events
        self.stop_event.clear()
        self.pause_event.clear()
        # start worker
        self.worker_thread = threading.Thread(target=self._worker, args=(input_dir, output_dir), daemon=True)
        self.worker_thread.start()
        self._log(f'Bắt đầu xử lý {len(files)} file')

    def toggle_pause(self):
        if not (self.worker_thread and self.worker_thread.is_alive()):
            # no worker running
            self._log('Không có tiến trình nào để tạm dừng/tiếp tục')
            return
        if not self.pause_event.is_set():
            # pause
            self.pause_event.set()
            self._pause_btn.config(text='Tiếp Tục')
            self._log('Đã tạm dừng')
        else:
            # resume
            self.pause_event.clear()
            self._pause_btn.config(text='Tạm Dừng')
            self._log('Tiếp tục xử lý')

    def exit_app(self):
        if messagebox.askokcancel('Thoát', 'Bạn có chắc muốn thoát không?'):
            # signal worker to stop
            self.stop_event.set()
            # if paused, resume so worker can exit cleanly
            self.pause_event.clear()
            # wait a short time for thread to finish
            if self.worker_thread and self.worker_thread.is_alive():
                self._log('Đang chờ worker dừng...')
                self.worker_thread.join(timeout=2)
            self.destroy()

    # ---------------- worker and processing ----------------
    def _worker(self, input_dir, output_dir):
        while not self.stop_event.is_set():
            try:
                filepath = self.file_queue.get_nowait()
            except queue.Empty:
                break
            # pause handling
            while self.pause_event.is_set() and not self.stop_event.is_set():
                time.sleep(0.2)

            if self.stop_event.is_set():
                break
            try:
                # perform conversion
                rel_path = os.path.relpath(filepath, input_dir)
                filename = os.path.splitext(os.path.basename(filepath))[0]
                out_path = os.path.join(output_dir, filename + '.ico')
                self._log(f'Chuyển: {rel_path} -> {out_path}')
                self._convert_image_to_ico(filepath, out_path)
                # update counters via event queue
                self.event_queue.put(('converted', filepath))
                self._log(f'Hoàn thành: {rel_path}')
            except Exception as e:
                self.event_queue.put(('error', filepath))
                self._log(f'Lỗi khi xử lý {filepath}: {e}')
            # small sleep to give GUI breathing room when many files
            time.sleep(0.02)
        self.event_queue.put(('finished', None))
        self._log('Worker kết thúc')

    def _convert_image_to_ico(self, src, dst):
        # Using Pillow to open and save as .ico with size 256x256
        with Image.open(src) as im:
            # convert to RGBA to preserve transparency if any
            if im.mode not in ('RGBA', 'RGB'):
                im = im.convert('RGBA')
            # resize while keeping aspect by fitting into ICON_SIZE with possible alpha
            im.thumbnail(ICON_SIZE, Image.LANCZOS)
            # create a new image of ICON_SIZE with transparent background and paste centered
            final = Image.new('RGBA', ICON_SIZE, (0,0,0,0))
            x = (ICON_SIZE[0] - im.width) // 2
            y = (ICON_SIZE[1] - im.height) // 2
            final.paste(im, (x, y), im if 'A' in im.getbands() else None)
            # Save as ICO. Pillow will write a valid .ico file. Use sizes parameter for safety.
            final.save(dst, format='ICO', sizes=[ICON_SIZE])

    # ---------------- event queue processing for GUI updates ----------------
    def _process_event_queue(self):
        processed_any = False
        while True:
            try:
                ev, payload = self.event_queue.get_nowait()
            except queue.Empty:
                break
            processed_any = True
            if ev == 'converted':
                self.converted_files.set(self.converted_files.get() + 1)
            elif ev == 'error':
                self.error_files.set(self.error_files.get() + 1)
            elif ev == 'finished':
                pass
            # update pending count = total - converted - errors
            pending = max(0, self.total_files.get() - self.converted_files.get() - self.error_files.get())
            self.pending_files.set(pending)
        # also periodically update pending if queue size changed externally
        if not processed_any:
            # sync pending with actual queue size if worker running
            if self.worker_thread and self.worker_thread.is_alive():
                qsize = self.file_queue.qsize()
                # qsize is number of yet-to-be-processed files
                # but total includes everything, so pending = qsize
                self.pending_files.set(qsize)
        self.after(200, self._process_event_queue)

    # ---------------- logging ----------------
    def _log(self, text):
        ts = time.strftime('%Y-%m-%d %H:%M:%S')
        msg = f'[{ts}] {text}\n'
        # Put into text widget in main thread by scheduling
        def append():
            self.log_text.configure(state='normal')
            self.log_text.insert('end', msg)
            self.log_text.see('end')
            self.log_text.configure(state='disabled')
        # schedule append on main thread
        self.after(1, append)

if __name__ == '__main__':
    app = ImagesToICOConverter()
    app.mainloop()
pyinstaller --noconsole --onefile --windowed --add-data "4-ImagesToICOConverter-icon.ico;." --icon=4-ImagesToICOConverter-icon.ico 4-ImagesToICOConverter-Source.py

Để lại một bình luận