Переименование окон 1С

30.07.26

База данных - Инструменты администратора БД

Приложение переименовывает открытые окна 1С с добавлением в начало имени открытой базы, чтобы сразу было понятно, где какая база открыта.

Файлы

ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.

Наименование Скачано Купить файл
Переименование окон 1С
.zip 30,39Mb
0 2 500 руб. Купить

Подписка PRO — скачивайте любые файлы со скидкой до 85% из Базы знаний

Оформите подписку на компанию для решения рабочих задач

Оформить подписку и скачать решение со скидкой

Вы можете заказать платную доработку или адаптацию этой разработки под вашу конфигурацию на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

# Переименование окон 1С


 

Приложение на Python, которое находится в системном трее и:

- сразу после запуска сканирует открытые окна;

- периодически повторяет сканирование;

- обрабатывает только окна процессов, имя которых содержит `1c`;

- определяет имя информационной базы по параметрам `/IBName`, `/IBConnectionString`, `/F` и `/S`;

- меняет заголовок по схеме `Имя базы — прежний заголовок`;

- не добавляет имя повторно, если такой префикс уже установлен;

- при штатном выходе восстанавливает прежние заголовки всех ещё открытых окон, переименованных текущим запуском;

- позволяет вручную запустить сканирование через меню значка в трее;

- позволяет выбрать готовый интервал сканирования или указать собственный в секундах;

- не допускает запуск второго экземпляра приложения;

- использует узнаваемую жёлто-красно-чёрную иконку 1С в трее и у собранного EXE.

 

## Запуск готового приложения

Приложение находится в папке dist

 

## Запуск скриптом без компиляции, используя python

1. Установите Python 3.11 или новее.

2. Запустите `install_and_run.bat`.

3. Значок `1C` появится в области уведомлений Windows.

Приложение запускается через `pythonw`, поэтому отдельное консольное окно не появляется.

 

## Сборка приложения

Запустите `build_exe.bat`.

При сборке файлы `1c_icon.png` и `1c_icon.ico` автоматически используются для значка в трее и значка EXE.

Результат появится здесь:

dist\1C-Window-Renamer.exe
 

## Меню в трее

- **Переименовать сейчас** — немедленно просканировать окна.

- **Интервал сканирования** — выбрать 5, 10, 30 секунд, 1 или 5 минут либо открыть пункт **Задать вручную…** и ввести значение от 1 до 86 400 секунд. Значение в поле можно полностью стереть или заменить; `Enter` подтверждает ввод, `Esc` отменяет.

- **Выход** — остановить сканирование, вернуть исходные заголовки переименованным окнам и завершить приложение.

По умолчанию используется интервал 10 секунд. Выбранное значение сохраняется между запусками в пользовательской папке `%APPDATA%\1CWindowRenamer`.

 

## Восстановление заголовков при выходе

Приложение запоминает исходный заголовок каждого окна, которое оно успешно переименовало. При выборе пункта **Выход** периодическое сканирование останавливается, открытым окнам возвращаются прежние заголовки, после чего приложение завершается.

Восстановление возможно только при штатном завершении приложения. При принудительном завершении процесса через Диспетчер задач код очистки выполнить невозможно.

 

## Как определяется имя базы

Ключи проверяются в таком порядке:

1. `/IBName` — используется указанное имя базы.

2. `/IBConnectionString` — для серверной базы берётся `Ref`, для файловой — имя последней папки из `File`.

3. `/F` — берётся имя последней папки файловой базы.

4. `/S` — берётся имя базы из строки `сервер\база`.

Если определить имя не удалось, окно не переименовывается.

 

## Настройки в коде

В начале файла `rename_1c_windows.py` доступны параметры:

DEFAULT_SCAN_INTERVAL_SECONDS = 10
AVAILABLE_SCAN_INTERVALS = (5, 10, 30, 60, 300)
MIN_SCAN_INTERVAL_SECONDS = 1
MAX_SCAN_INTERVAL_SECONDS = 86_400
PROCESS_NAME_FRAGMENT = "1c"
TITLE_SEPARATOR = " — "
RENAME_OWNED_WINDOWS = False

При `RENAME_OWNED_WINDOWS = False` переименовываются только основные окна. Чтобы также менять заголовки принадлежащих процессу диалогов 1С, установите `True`.

 

## Права доступа

Если 1С запущена с повышенными правами, приложение также следует запускать от администратора. Иначе Windows может запретить чтение параметров процесса или изменение заголовка окна.


## Код отдельно, доступный для копирования
 

from __future__ import annotations

import ctypes
import json
import os
import re
import sys
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional

import psutil
import pystray
import win32con
import win32gui
import win32process
from PIL import Image, ImageDraw


# ----------------------------- Настройки -----------------------------------

DEFAULT_SCAN_INTERVAL_SECONDS = 10
AVAILABLE_SCAN_INTERVALS = (5, 10, 30, 60, 300)
MIN_SCAN_INTERVAL_SECONDS = 1
MAX_SCAN_INTERVAL_SECONDS = 86_400
PROCESS_NAME_FRAGMENT = "1c"
TITLE_SEPARATOR = " — "

# False: переименовывать только основные окна 1С.
# True: также переименовывать диалоги и другие принадлежащие процессу окна.
RENAME_OWNED_WINDOWS = False

APP_NAME = "1C Window Renamer"
ICON_FILENAME = "1c_icon.png"
MUTEX_NAME = r"Local\OneCWindowRenamer"


# -------------------------- Хранение настроек ------------------------------


def settings_file_path() -> Path:
    app_data = os.environ.get("APPDATA")
    if app_data:
        settings_directory = Path(app_data) / "1CWindowRenamer"
    else:
        settings_directory = Path.home() / ".1c_window_renamer"
    return settings_directory / "settings.json"


def load_scan_interval() -> int:
    try:
        data = json.loads(settings_file_path().read_text(encoding="utf-8"))
        interval = int(data.get("scan_interval_seconds", DEFAULT_SCAN_INTERVAL_SECONDS))
        if MIN_SCAN_INTERVAL_SECONDS <= interval <= MAX_SCAN_INTERVAL_SECONDS:
            return interval
    except (OSError, ValueError, TypeError, json.JSONDecodeError):
        pass

    return DEFAULT_SCAN_INTERVAL_SECONDS


def save_scan_interval(interval: int) -> None:
    if not MIN_SCAN_INTERVAL_SECONDS <= interval <= MAX_SCAN_INTERVAL_SECONDS:
        return

    try:
        path = settings_file_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(
            json.dumps(
                {"scan_interval_seconds": interval},
                ensure_ascii=False,
                indent=2,
            ),
            encoding="utf-8",
        )
    except OSError:
        # Настройка продолжит действовать до завершения текущего запуска.
        pass


# --------------------------- Определение базы -------------------------------


@dataclass(frozen=True)
class ProcessInfo:
    process_name: str
    base_name: Optional[str]


@dataclass(frozen=True)
class RenamedWindowInfo:
    process_id: int
    prefix: str
    original_title: str
    renamed_title: str


def clean_value(value: str) -> str:
    """Убирает внешние кавычки и лишние пробелы из значения параметра."""
    value = value.strip()
    while len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
        value = value[1:-1].strip()
    return value


def get_switch_value(arguments: list[str], switch: str) -> Optional[str]:
    """
    Находит параметр командной строки в вариантах:
      /IBName "Имя"
      /IBName"Имя"
      /IBName=Имя
    """
    switch_lower = switch.lower()

    for index, raw_argument in enumerate(arguments):
        argument = raw_argument.strip()
        argument_lower = argument.lower()

        if argument_lower == switch_lower:
            if index + 1 < len(arguments):
                value = clean_value(arguments[index + 1])
                return value or None
            return None

        if not argument_lower.startswith(switch_lower):
            continue

        remainder = argument[len(switch):].strip()
        if not remainder:
            continue

        # Не считаем /SomeKey значением короткого ключа /S или /F.
        # При этом поддерживаем штатные формы /Sserver\base и /FC:\Base.
        if remainder[0] not in {'=', '"', "'"} and switch_lower in {"/s", "/f"}:
            if not any(separator in remainder for separator in ("\\", "/", ":")):
                continue

        if remainder.startswith("="):
            remainder = remainder[1:].strip()

        value = clean_value(remainder)
        if value:
            return value

    return None


def base_name_from_file_path(value: str) -> Optional[str]:
    """Получает имя файловой базы из пути /F или File=... ."""
    value = clean_value(value).rstrip("\\/")
    if not value:
        return None

    normalized = value.replace("/", "\\")
    base_name = normalized.rsplit("\\", 1)[-1].strip()
    return base_name or value


def base_name_from_server_path(value: str) -> Optional[str]:
    """Получает имя серверной базы из значения /S сервер\база."""
    value = clean_value(value).strip().rstrip("\\/")
    if not value:
        return None

    normalized = value.replace("/", "\\")
    base_name = normalized.rsplit("\\", 1)[-1].strip()
    return base_name or value


def connection_string_parameter(connection_string: str, parameter: str) -> Optional[str]:
    """Извлекает параметр из строки соединения 1С без учёта регистра."""
    pattern = re.compile(
        rf"(?:^|;)\s*{re.escape(parameter)}\s*=\s*(?:\"([^\"]*)\"|'([^']*)'|([^;]*))",
        re.IGNORECASE,
    )
    match = pattern.search(clean_value(connection_string))
    if not match:
        return None

    value = next((group for group in match.groups() if group is not None), "")
    value = clean_value(value)
    return value or None


def base_name_from_connection_string(connection_string: str) -> Optional[str]:
    """Определяет базу из /IBConnectionString."""
    reference = connection_string_parameter(connection_string, "Ref")
    if reference:
        return reference

    file_path = connection_string_parameter(connection_string, "File")
    if file_path:
        return base_name_from_file_path(file_path)

    return None


def detect_base_name(command_line: Iterable[str]) -> Optional[str]:
    """
    Определяет имя базы по четырём вариантам запуска 1С.

    Приоритет:
      1. /IBName
      2. /IBConnectionString
      3. /F
      4. /S
    """
    arguments = [str(argument) for argument in command_line]

    ib_name = get_switch_value(arguments, "/IBName")
    if ib_name:
        return ib_name

    connection_string = get_switch_value(arguments, "/IBConnectionString")
    if connection_string:
        base_name = base_name_from_connection_string(connection_string)
        if base_name:
            return base_name

    file_path = get_switch_value(arguments, "/F")
    if file_path:
        return base_name_from_file_path(file_path)

    server_path = get_switch_value(arguments, "/S")
    if server_path:
        return base_name_from_server_path(server_path)

    return None


# ------------------------- Работа с окнами Windows --------------------------


class WindowRenamer:
    def __init__(self) -> None:
        self._scan_lock = threading.Lock()
        self._process_cache: dict[int, Optional[ProcessInfo]] = {}
        self._renamed_windows: dict[int, RenamedWindowInfo] = {}

    def scan_and_rename(self) -> int:
        """Сканирует открытые окна и возвращает число переименованных окон."""
        if not self._scan_lock.acquire(blocking=False):
            return 0

        try:
            self._process_cache.clear()
            self._remove_closed_tracked_windows()
            renamed_count = 0

            def enum_callback(hwnd: int, _: object) -> bool:
                nonlocal renamed_count
                try:
                    if self._rename_window_if_needed(hwnd):
                        renamed_count += 1
                except Exception:
                    # Ошибка одного окна не должна прерывать общее сканирование.
                    pass
                return True

            win32gui.EnumWindows(enum_callback, None)
            return renamed_count
        except Exception:
            return 0
        finally:
            self._scan_lock.release()

    def _rename_window_if_needed(self, hwnd: int) -> bool:
        if not win32gui.IsWindow(hwnd) or not win32gui.IsWindowVisible(hwnd):
            return False

        if not RENAME_OWNED_WINDOWS and win32gui.GetWindow(hwnd, win32con.GW_OWNER):
            return False

        old_title = win32gui.GetWindowText(hwnd).strip()
        if not old_title:
            return False

        _, process_id = win32process.GetWindowThreadProcessId(hwnd)
        process_info = self._get_process_info(process_id)
        if process_info is None or not process_info.base_name:
            return False

        prefix = f"{process_info.base_name}{TITLE_SEPARATOR}"
        if old_title.startswith(prefix):
            self._refresh_tracked_title(hwnd, process_id, prefix, old_title)
            return False

        new_title = f"{prefix}{old_title}"
        win32gui.SetWindowText(hwnd, new_title)

        if win32gui.GetWindowText(hwnd) != new_title:
            return False

        self._renamed_windows[hwnd] = RenamedWindowInfo(
            process_id=process_id,
            prefix=prefix,
            original_title=old_title,
            renamed_title=new_title,
        )
        return True

    def restore_original_titles(self) -> int:
        """Возвращает исходные заголовки окнам, переименованным этим запуском."""
        restored_count = 0

        with self._scan_lock:
            tracked_windows = list(self._renamed_windows.items())
            self._renamed_windows.clear()

            for hwnd, window_info in tracked_windows:
                try:
                    if not win32gui.IsWindow(hwnd):
                        continue

                    _, process_id = win32process.GetWindowThreadProcessId(hwnd)
                    if process_id != window_info.process_id:
                        # HWND мог быть повторно использован другим процессом.
                        continue

                    current_title = win32gui.GetWindowText(hwnd)
                    if current_title == window_info.renamed_title:
                        restored_title = window_info.original_title
                    elif current_title.startswith(window_info.prefix):
                        # Если 1С успела изменить часть заголовка после префикса,
                        # удаляем только добавленный приложением префикс.
                        restored_title = current_title[len(window_info.prefix):]
                    else:
                        # Заголовок уже изменён сторонним кодом или самой 1С.
                        continue

                    if not restored_title:
                        continue

                    win32gui.SetWindowText(hwnd, restored_title)
                    if win32gui.GetWindowText(hwnd) == restored_title:
                        restored_count += 1
                except Exception:
                    # Ошибка одного окна не должна мешать восстановлению остальных.
                    pass

        return restored_count

    def _refresh_tracked_title(
        self,
        hwnd: int,
        process_id: int,
        prefix: str,
        current_title: str,
    ) -> None:
        """Обновляет сохранённый заголовок, если 1С изменила его после префикса."""
        tracked = self._renamed_windows.get(hwnd)
        if tracked is None or tracked.process_id != process_id:
            return

        original_title = current_title[len(prefix):]
        if not original_title:
            return

        self._renamed_windows[hwnd] = RenamedWindowInfo(
            process_id=process_id,
            prefix=prefix,
            original_title=original_title,
            renamed_title=current_title,
        )

    def _remove_closed_tracked_windows(self) -> None:
        """Удаляет записи о закрытых окнах и повторно использованных HWND."""
        for hwnd, window_info in list(self._renamed_windows.items()):
            try:
                if not win32gui.IsWindow(hwnd):
                    del self._renamed_windows[hwnd]
                    continue

                _, process_id = win32process.GetWindowThreadProcessId(hwnd)
                if process_id != window_info.process_id:
                    del self._renamed_windows[hwnd]
            except Exception:
                self._renamed_windows.pop(hwnd, None)

    def _get_process_info(self, process_id: int) -> Optional[ProcessInfo]:
        if process_id in self._process_cache:
            return self._process_cache[process_id]

        result: Optional[ProcessInfo] = None

        try:
            process = psutil.Process(process_id)
            with process.oneshot():
                process_name = process.name()
                if PROCESS_NAME_FRAGMENT.lower() not in process_name.lower():
                    self._process_cache[process_id] = None
                    return None

                command_line = process.cmdline()
                result = ProcessInfo(
                    process_name=process_name,
                    base_name=detect_base_name(command_line),
                )
        except (psutil.NoSuchProcess, psutil.ZombieProcess, psutil.AccessDenied):
            result = None
        except Exception:
            result = None

        self._process_cache[process_id] = result
        return result


# ------------------------------ Ресурсы -------------------------------------


def application_resource_path(file_name: str) -> Path:
    """Возвращает путь к ресурсу при запуске из исходников и из PyInstaller EXE."""
    bundle_directory = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent))
    return bundle_directory / file_name


# ------------------------------ Трей ----------------------------------------


class TrayApplication:
    def __init__(self) -> None:
        self._stop_event = threading.Event()
        self._scanner_wakeup_event = threading.Event()
        self._interval_lock = threading.Lock()
        self._shutdown_lock = threading.Lock()
        self._custom_interval_dialog_lock = threading.Lock()
        self._shutdown_complete = False
        self._scan_interval = load_scan_interval()
        self._renamer = WindowRenamer()

        self._icon = pystray.Icon(
            name="one_c_window_renamer",
            icon=self._create_icon(),
            title="Переименование окон 1С",
            menu=self._create_menu(),
        )
        self._worker = threading.Thread(
            target=self._scanner_loop,
            name="window-scanner",
            daemon=True,
        )

    def _create_menu(self) -> pystray.Menu:
        interval_items = []
        for interval in AVAILABLE_SCAN_INTERVALS:
            interval_items.append(
                pystray.MenuItem(
                    self._format_interval(interval),
                    self._make_interval_handler(interval),
                    checked=self._make_interval_checker(interval),
                    radio=True,
                )
            )

        interval_items.extend(
            (
                pystray.Menu.SEPARATOR,
                pystray.MenuItem(
                    "Задать вручную…",
                    self._set_custom_interval,
                ),
            )
        )

        return pystray.Menu(
            pystray.MenuItem(
                "Переименовать сейчас",
                self._scan_now,
                default=True,
            ),
            pystray.MenuItem(
                "Интервал сканирования",
                pystray.Menu(*interval_items),
            ),
            pystray.Menu.SEPARATOR,
            pystray.MenuItem("Выход", self._exit),
        )

    def run(self) -> None:
        try:
            self._worker.start()
            self._icon.run()
        finally:
            self._shutdown()

    def _scanner_loop(self) -> None:
        # Первое сканирование выполняется сразу после запуска.
        while not self._stop_event.is_set():
            self._renamer.scan_and_rename()

            self._scanner_wakeup_event.clear()
            interval = self._get_scan_interval()
            self._scanner_wakeup_event.wait(interval)

    def _get_scan_interval(self) -> int:
        with self._interval_lock:
            return self._scan_interval

    def _set_scan_interval(self, interval: int) -> None:
        if not MIN_SCAN_INTERVAL_SECONDS <= interval <= MAX_SCAN_INTERVAL_SECONDS:
            return

        with self._interval_lock:
            self._scan_interval = interval

        save_scan_interval(interval)
        self._scanner_wakeup_event.set()
        self._icon.update_menu()

    def _set_custom_interval(
        self,
        _icon: pystray.Icon,
        _item: pystray.MenuItem,
    ) -> None:
        # Окно Tk запускается в отдельном UI-потоке. Так оно не конфликтует
        # с циклом обработки сообщений значка pystray и нормально принимает ввод.
        threading.Thread(
            target=self._show_custom_interval_dialog,
            name="custom-interval-dialog",
            daemon=True,
        ).start()

    def _show_custom_interval_dialog(self) -> None:
        if not self._custom_interval_dialog_lock.acquire(blocking=False):
            return

        root = None
        try:
            import tkinter as tk
            from tkinter import messagebox

            root = tk.Tk()
            root.title("Интервал сканирования")
            root.resizable(False, False)
            root.attributes("-topmost", True)

            container = tk.Frame(root, padx=14, pady=12)
            container.pack(fill="both", expand=True)

            label = tk.Label(
                container,
                text="Введите интервал сканирования в секундах:",
                anchor="w",
            )
            label.pack(fill="x")

            interval_value = tk.StringVar(value=str(self._get_scan_interval()))
            entry = tk.Entry(
                container,
                textvariable=interval_value,
                width=32,
                state="normal",
            )
            entry.pack(fill="x", pady=(5, 12))

            buttons = tk.Frame(container)
            buttons.pack(fill="x")

            def close_dialog() -> None:
                root.quit()

            def apply_interval() -> None:
                raw_value = interval_value.get().strip()
                try:
                    interval = int(raw_value)
                except ValueError:
                    messagebox.showerror(
                        "Некорректное значение",
                        "Введите целое число секунд.",
                        parent=root,
                    )
                    entry.focus_force()
                    entry.selection_range(0, tk.END)
                    return

                if not MIN_SCAN_INTERVAL_SECONDS <= interval <= MAX_SCAN_INTERVAL_SECONDS:
                    messagebox.showerror(
                        "Некорректное значение",
                        (
                            "Введите значение от "
                            f"{MIN_SCAN_INTERVAL_SECONDS} до "
                            f"{MAX_SCAN_INTERVAL_SECONDS} секунд."
                        ),
                        parent=root,
                    )
                    entry.focus_force()
                    entry.selection_range(0, tk.END)
                    return

                self._set_scan_interval(interval)
                close_dialog()

            ok_button = tk.Button(
                buttons,
                text="OK",
                width=12,
                command=apply_interval,
                default="active",
            )
            ok_button.pack(side="left", padx=(38, 8))

            cancel_button = tk.Button(
                buttons,
                text="Отмена",
                width=12,
                command=close_dialog,
            )
            cancel_button.pack(side="left")

            root.bind("<Return>", lambda _event: apply_interval())
            root.bind("<Escape>", lambda _event: close_dialog())
            root.protocol("WM_DELETE_WINDOW", close_dialog)

            root.update_idletasks()
            window_width = root.winfo_reqwidth()
            window_height = root.winfo_reqheight()
            screen_width = root.winfo_screenwidth()
            screen_height = root.winfo_screenheight()
            position_x = max(0, (screen_width - window_width) // 2)
            position_y = max(0, (screen_height - window_height) // 2)
            root.geometry(f"+{position_x}+{position_y}")

            def activate_entry() -> None:
                root.lift()
                root.focus_force()
                entry.configure(state="normal")
                entry.focus_force()
                entry.selection_range(0, tk.END)
                entry.icursor(tk.END)

            root.after(100, activate_entry)
            root.mainloop()
        except Exception as error:
            ctypes.windll.user32.MessageBoxW(
                None,
                f"Не удалось открыть окно ввода интервала:\n{error}",
                "Переименование окон 1С",
                0x10,
            )
        finally:
            if root is not None:
                try:
                    root.destroy()
                except Exception:
                    pass
            self._custom_interval_dialog_lock.release()

    def _make_interval_handler(self, interval: int):
        def handler(_icon: pystray.Icon, _item: pystray.MenuItem) -> None:
            self._set_scan_interval(interval)

        return handler

    def _make_interval_checker(self, interval: int):
        def checker(_item: pystray.MenuItem) -> bool:
            return self._get_scan_interval() == interval

        return checker

    def _scan_now(self, _icon: pystray.Icon, _item: pystray.MenuItem) -> None:
        threading.Thread(
            target=self._renamer.scan_and_rename,
            name="manual-window-scan",
            daemon=True,
        ).start()

    def _exit(self, icon: pystray.Icon, _item: pystray.MenuItem) -> None:
        self._stop_event.set()
        self._scanner_wakeup_event.set()
        icon.stop()

    def _shutdown(self) -> None:
        """Останавливает сканирование и восстанавливает изменённые заголовки."""
        with self._shutdown_lock:
            if self._shutdown_complete:
                return

            self._stop_event.set()
            self._scanner_wakeup_event.set()

            if self._worker.is_alive() and threading.current_thread() is not self._worker:
                self._worker.join()

            self._renamer.restore_original_titles()
            self._shutdown_complete = True

    @staticmethod
    def _format_interval(interval: int) -> str:
        if interval < 60:
            return f"{interval} сек."
        if interval == 60:
            return "1 мин."
        return f"{interval // 60} мин."

    @staticmethod
    def _create_icon() -> Image.Image:
        icon_path = application_resource_path(ICON_FILENAME)

        try:
            with Image.open(icon_path) as source_image:
                return source_image.convert("RGBA").resize((64, 64), Image.Resampling.LANCZOS)
        except (OSError, ValueError):
            # Резервная иконка на случай повреждения или отсутствия ресурса.
            size = 64
            image = Image.new("RGBA", (size, size), (255, 221, 0, 255))
            draw = ImageDraw.Draw(image)
            draw.rounded_rectangle(
                (2, 2, size - 3, size - 3),
                radius=8,
                outline=(30, 30, 30, 255),
                width=3,
            )
            draw.text((10, 18), "1", fill=(226, 20, 38, 255), stroke_width=1)
            draw.text((33, 18), "C", fill=(20, 20, 20, 255), stroke_width=1)
            return image


# -------------------------- Один экземпляр ---------------------------------


_mutex_handle: Optional[int] = None


def acquire_single_instance_mutex() -> bool:
    global _mutex_handle

    kernel32 = ctypes.windll.kernel32
    kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, ctypes.c_bool, ctypes.c_wchar_p]
    kernel32.CreateMutexW.restype = ctypes.c_void_p

    _mutex_handle = kernel32.CreateMutexW(None, False, MUTEX_NAME)
    already_exists = kernel32.GetLastError() == 183  # ERROR_ALREADY_EXISTS

    if already_exists:
        ctypes.windll.user32.MessageBoxW(
            None,
            "Приложение уже запущено и находится в системном трее.",
            "Переименование окон 1С",
            0x40,
        )
        return False

    return True


def main() -> None:
    if not acquire_single_instance_mutex():
        return

    try:
        TrayApplication().run()
    except Exception as error:
        ctypes.windll.user32.MessageBoxW(
            None,
            f"Приложение завершено из-за ошибки:\n{error}",
            "Переименование окон 1С",
            0x10,
        )


if __name__ == "__main__":
    main()

 

Вступайте в нашу телеграмм-группу Инфостарт

окна окно конфигуратор предприятие переименовать переименование заголовок заголовки имя база ренеймер

См. также

Инструментарий разработчика Роли и права Запросы СКД Программист Руководитель проекта 1С:Предприятие 8 Платные (руб)

Инструменты для разработчиков 1С 8.3: Infostart Toolkit. Автоматизация и ускорение разработки на управляемых формах. Легкость работы с 1С.

10000 руб.

02.09.2020    269079    1497    422    

1180

Инструментарий разработчика Чистка данных Свертка базы Инструменты администратора БД Системный администратор Программист Руководитель проекта 1С:Предприятие 8 1С:ERP Управление предприятием 2 1С:Бухгалтерия 3.0 1С:Управление торговлей 11 1С:Комплексная автоматизация 2.х 1С:Управление нашей фирмой 3.0 Россия Платные (руб)

Инструмент представляет собой обработку для проведения свёртки или обрезки баз данных. Работает на ЛЮБЫХ конфигурациях (УТ, БП, ERP, УНФ, КА и т.д.). Поддерживаются серверные и файловые базы, управляемые и обычные формы, интерфейс 8.5. Может выполнять свертку одновременно в несколько потоков, а также без непосредственного участия пользователя. Решение в Реестре отечественного ПО.

24900 руб.

20.08.2024    75500    381    170    

326

Пакетная печать Печатные формы Инструментарий разработчика Программист 1С:Предприятие 8 Платные (руб)

Расширение для создания и редактирования печатных форм в системе 1С:Предприятие 8.3. Благодаря конструктору можно значительно снизить затраты времени на разработку печатных форм, повысить качество и прозрачность разработки, а также навести порядок в многообразии корпоративных печатных форм. Обновление версии от 21.04.26

22570 руб.

06.10.2023    40322    115    54    

128

Инструментарий разработчика Нейросети Платные (руб)

Первые попытки разработки на 1С с использованием больших языковых моделей (LLM) могут разочаровать. LLMки сильно галлюцинируют, потому что не знают устройства конфигураций 1С, не знают нюансов синтаксиса. Но если дать им подсказки с помощью MCP, то результат получается кардинально лучше. Далее в публикации: MCP для поиска по метаданным 1С, справке синтакс-помощника и проверки синтаксиса.

15250 руб.

25.08.2025    64270    130    36    

139

Инструментарий разработчика Разработка Администрирование веб-серверов Системный администратор Программист Бизнес-аналитик Руководитель проекта 1С 8.3 Платные (руб)

Analyzer 1C сводит выгрузку 1С — основную конфигурацию и все расширения — в единый граф знаний. Любой запрос по связям за доли секунды, с пометками «Доб.» / «Заимств.» / «Переопределено». Новое в 2.0 — обновление поставки: сравнение и объединение версий деревом «как в Конфигураторе» с выгрузкой плана решений; поиск конфликтов из-за перехватов расширений и висячих ссылок; загрузка из бинарных .cf/.cfe; циклические зависимости. Плюс анализ влияния, запросы BSL, роли и RLS, граф вызовов. Минута на развёртывание через Docker без необходимости подключения к Интернет. Любая 1С:Предприятие 8.3+.

14000 руб.

17.04.2026    9135    39    52    

54

Разработка Инструменты администратора БД Администрирование веб-серверов Администрирование Программист 1C:ERP Платные (руб)

Это специализированное решение для глубокого анализа и мониторинга серверов и баз данных 1С. Продукт позволяет выявлять причины замедлений, блокировок и ошибок, объединяя данные технологического журнала, СУБД и оборудования в единой интерактивной системе.

90000 руб.

13.05.2026    1363    2    0    

4

Инструменты администратора БД Инструментарий разработчика Роли и права Программист 1С:Предприятие 8 1C:Бухгалтерия Россия Платные (руб)

Расширение позволяет без изменения кода конфигурации выполнять проверки при вводе данных, скрывать от пользователя недоступные ему данные, выполнять код в обработчиках. Не изменяет данные конфигурации, легко устанавливается практически на любую конфигурацию на управляемых формах.

17000 руб.

10.11.2023    26524    98    46    

104

Закрытие периода Инструменты администратора БД Корректировка данных Бухгалтер Пользователь 1С:Предприятие 8 1С:Бухгалтерия 3.0 Россия Бухгалтерский учет Платные (руб)

Расширение «Оперативное проведение» в 4 раза уменьшает время проведения документов и закрытия месяца. Является комплексным решением проблем 62 и 60 счетов. Оптимизирует проведение при включенной функциональной опции «Раздельный учет НДС». Используется в более 10 организациях уже 2 года. Совместимо с конфигурацией Бухгалтерия 3.0 (+КОРП).

14640 руб.

29.04.2020    51396    141    162    

96
Для отправки сообщения требуется регистрация/авторизация