포스트

CustomWindow - DComp 로직

CustomWindow - DComp 로직

1. UI

UI 단에서 사용자가 버튼을 누르면 AutoWindowChange 속성이 값이 변하면서 setter가 작동해 아래와 같은 코드가 먼저 실행된다.

1.1. 토글 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    public bool AutoWindowChange
    {
        get => _config.AutoWindowChange;
        set
        {
            if (_config.AutoWindowChange == value) return;
            _config.AutoWindowChange = value;
            OnPropertyChanged();
            
            if (value)
            {
                if (!BorderServiceAvailable)
                {
                    WindowTracker.AddExternalLog("안내: BorderService DLL이 없어도 EXE 모드로 동작합니다.");
                }
                
                WindowTracker.Start();
                // BorderService 시작
                var borderHex = _config.BorderColor ?? "#FF0000"; // 기본 색상
                int thickness = _config.BorderThickness;
                BorderService.SetConsoleVisibilityPreference(_config.ShowBorderServiceConsole);
                BorderService.SetRenderModePreference(_config.BorderRenderMode);
                BorderService.SetForegroundWindowOnly(_config.ForegroundWindowOnly); // 새로운 옵션 적용
                BorderService.StartIfNeeded(borderHex, thickness, _config.Snapshot.ExcludedPrograms.ToArray());
                
                // (DLL 가용 시) 추가 설정
                if (BorderServiceAvailable && BorderService.IsRunning)
                {
                    BorderService.SetPartialRatio(0.3f); // 30% 부분 렌더
                    BorderService.EnableMerge(true);     // 머지 활성화
                }
                
                WindowTracker.AddExternalLog("AutoWindowChange ON: BorderService 시작 요청");
            }
            else
            {
                BorderService.StopIfRunning();
                WindowTracker.Stop();
                WindowTracker.AddExternalLog("AutoWindowChange OFF: BorderService 중지");
            }

            // 상태 갱신
            CheckBorderServiceStatus();
        }
    }

이 코드의 대략적인 순서도는 다음과 같다.

flowchart TD
    A[AutoWindowChange setter 호출] --> B{value가 현재값과 같은가?}
    B -->|Yes| C[return - 종료]
    B -->|No| D[_config.AutoWindowChange = value<br/>OnPropertyChanged 호출]
    
    D --> E{value가 true인가?}
    
    E -->|true| F{BorderServiceAvailable?}
    F -->|false| G[WindowTracker.AddExternalLog<br/>'DLL 없어도 EXE 모드로 동작']
    F -->|true| H[WindowTracker.Start 호출]
    G --> H
    
    H --> I[BorderService 설정<br/>- borderHex 설정<br/>- thickness 설정]
    I --> J[BorderService 옵션 설정<br/>- ConsoleVisibility<br/>- RenderMode<br/>- ForegroundWindowOnly]
    J --> K[BorderService.StartIfNeeded 호출]
    
    K --> L{BorderServiceAvailable &&<br/>BorderService.IsRunning?}
    L -->|true| M[추가 설정<br/>- SetPartialRatio 30%<br/>- EnableMerge true]
    L -->|false| N[WindowTracker.AddExternalLog<br/>'BorderService 시작 요청']
    M --> N
    
    E -->|false| O[BorderService.StopIfRunning]
    O --> P[WindowTracker.Stop]
    P --> Q[WindowTracker.AddExternalLog<br/>'BorderService 중지']
    
    N --> R[CheckBorderServiceStatus 호출]
    Q --> R
    R --> S[완료]
    
    style A fill:#e1f5fe
    style C fill:#ffcdd2
    style S fill:#c8e6c9
    style E fill:#fff3e0
    style F fill:#fff3e0
    style L fill:#fff3e0

위 코드에서 WindowTracker.Start()를 면저 살펴보자.

1.1.1. WindowTracker

우선 WindowTrack 클래스를 설명하기 위해 이 클래스의 멤버 변수와 서브 클래스를 소개한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    private static readonly ConcurrentDictionary<nint, TrackedWindow> _windows = new();
    private static Timer? _timer;
    private static readonly object _sync = new();
    private static bool _running;
    private static TimeSpan _interval = TimeSpan.FromMilliseconds(1000);

    private static readonly int _logCapacity = 500;
    private static readonly ConcurrentQueue<string> _logs = new();
    public static event Action<string>? LogAdded;

    // New: notify when window set changes (for EXE IPC)
    public static event Action<IReadOnlyCollection<nint>>? WindowSetChanged;

    /// <summary>현재 추적 중인 윈도우 핸들 목록.</summary>
    public static IReadOnlyCollection<nint> CurrentWindowHandles => _windows.Keys.ToList();

    /// <summary>최근 로그 전체 반환 (최신순)</summary>
    public static IReadOnlyList<string> GetRecentLogs() => _logs.Reverse().ToList();

    // New: provide process names along with handles for filtering (excluded list etc.)
    public static IReadOnlyList<(nint Handle, string? ProcessName)> GetCurrentWindowsDetailed() => _windows.Values.Select(w => (w.Handle, w.ProcessName)).ToList();
1
2
3
4
5
6
7
8
    private class TrackedWindow
    {
        public nint Handle { get; init; }
        public int ProcessId { get; init; }
        public string? ProcessName { get; init; }
        public DateTime FirstSeen { get; init; } = DateTime.UtcNow;
        public DateTime LastSeen { get; set; } = DateTime.UtcNow;
    }

멤버 변수 (Member Variables)

변수명데이터 타입접근자용도
_windowsConcurrentDictionary<nint, TrackedWindow>private static readonly현재 추적 중인 윈도우들을 저장하는 스레드 안전 딕셔너리
_timerTimer?private static주기적으로 윈도우 상태를 확인하는 타이머
_syncobjectprivate static readonly스레드 동기화를 위한 락 객체
_runningboolprivate static윈도우 추적이 실행 중인지 나타내는 플래그
_intervalTimeSpanprivate static윈도우 상태 확인 주기 (기본값: 1000ms)
_logCapacityintprivate static readonly로그 저장 최대 용량 (500개)
_logsConcurrentQueue<string>private static readonly로그 메시지들을 저장하는 스레드 안전 큐

이벤트 (Events)

이벤트명데이터 타입접근자용도
LogAddedAction<string>?public static새로운 로그가 추가될 때 발생하는 이벤트
WindowSetChangedAction<IReadOnlyCollection<nint>>?public static윈도우 집합이 변경될 때 발생하는 이벤트 (EXE IPC용)

속성 (Properties)

속성명데이터 타입접근자용도
CurrentWindowHandlesIReadOnlyCollection<nint>public static현재 추적 중인 윈도우 핸들 목록을 읽기 전용으로 반환

서브 클래스: TrackedWindow

속성명데이터 타입접근자초기값용도
Handlenintpublic init-윈도우 핸들 (고유 식별자)
ProcessIdintpublic init-윈도우를 소유한 프로세스 ID
ProcessNamestring?public init-프로세스 이름 (nullable)
FirstSeenDateTimepublic initDateTime.UtcNow윈도우가 처음 발견된 시각
LastSeenDateTimepublic setDateTime.UtcNow윈도우가 마지막으로 확인된 시각

주요 메서드 반환 타입

메서드명반환 타입용도
GetRecentLogs()IReadOnlyList<string>최근 로그 전체를 최신순으로 반환
GetCurrentWindowsDetailed()IReadOnlyList<(nint Handle, string? ProcessName)>현재 윈도우들의 핸들과 프로세스명을 함께 반환 (필터링용)

1.1.1. Start 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public static void Start(TimeSpan? interval = null)
    {
        lock (_sync)
        {
            if (_running)
            {
                AddLog("이미 실행 중이라 Start 무시");
                return;
            }
            if (interval != null) _interval = interval.Value;
            _running = true;
            _timer = new Timer(_ => Tick(), null, TimeSpan.Zero, _interval);
            AddLog($"추적 시작 (주기={_interval.TotalMilliseconds}ms)");
        }
    }

lock 키워드로 해당 리소스에는 하나의 스레드만 사용할 수 있도록 제한하고, 반복 이벤트를 위한 Timer 클래스의 초기화를 진행한다. 이 과정에서 타이머는 Tick() 함수를 반복적으로 실행한다.

1.1.2. Tick 함수란?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    private static void Tick()
    {
        try
        {
            var now = DateTime.UtcNow;
            var seen = new HashSet<nint>();
            int skippedInvisible = 0;
            int addedCount = 0;
            EnumWindows((hwnd, lparam) =>
            {
                if (!IsWindowVisible(hwnd)) { skippedInvisible++; return true; }
                if (IsIconic(hwnd)) return true; // 최소화 창 제외
                if (!HasNonEmptyTitle(hwnd)) return true; // 캡션 없는 창 제외
                if (!HasUsableRect(hwnd)) return true; // zero / off-screen
                GetWindowThreadProcessId(hwnd, out var pid);
                if (pid == 0) return true;
                var handle = (nint)hwnd;
                bool added = false;
                _windows.AddOrUpdate(handle,
                    _ =>
                    {
                        added = true;
                        return new TrackedWindow
                        {
                            Handle = handle,
                            ProcessId = (int)pid,
                            ProcessName = SafeGetProcessName(pid),
                            FirstSeen = now,
                            LastSeen = now
                        };
                    },
                    (_, existing) => { existing.LastSeen = now; return existing; });
                if (added) { addedCount++; AddLog($"추가: 0x{handle.ToInt64():X} PID={pid} {(SafeGetProcessName(pid) ?? "?")}"); }
                seen.Add(handle);
                return true;
            }, IntPtr.Zero);

            int removed = 0;
            foreach (var kv in _windows.ToArray())
            {
                if (!seen.Contains(kv.Key) || !IsWindow(kv.Key))
                {
                    if (_windows.TryRemove(kv.Key, out _))
                        removed++;
                }
            }
            AddLog($"Tick: Active={seen.Count} Added={addedCount} Removed={removed} SkipInvisible={skippedInvisible}");

            // Notify subscribers with the current set
            try { WindowSetChanged?.Invoke(seen.ToArray()); } catch { }
        }
        catch (Exception ex)
        {
            AddLog($"오류: {ex.Message}");
        }
    }
}

모든 윈도우의 핸들러들을 수집해, 사용자 눈에 보이는 윈도우인지 판단하고, 해당 핸들러들을 저장하며, 해당 핸들러가 이미 사용자 또는 외부 요인에 의해 닫힌 윈도우일 경우, 제거하는 역할을 한다.

1.1.2.1. HasNonEmptyTitle
1
2
3
4
5
6
7
8
    private static bool HasNonEmptyTitle(nint hwnd)
    {
        int len = GetWindowTextLength(hwnd);
        if (len <= 0 || len > 512) return false; // 유한한 한도
        var sb = new StringBuilder(len + 1);
        if (GetWindowText(hwnd, sb, sb.Capacity) <= 0) return false;
        return sb.ToString().Trim().Length > 0;
    }

이 코드는 해당 윈도우의 제목이 글자를 가지고 있는지를 판단하는 함수이다.

1.1.2.2. HasUsableRect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private static bool HasUsableRect(nint hwnd)
    {
        RECT rc;
        if (!GetWindowRect(hwnd, out rc)) return false;
        int w = rc.Right - rc.Left;
        int h = rc.Bottom - rc.Top;
        if (w <= 0 || h <= 0) return false;
        // basic off-screen check against virtual screen
        int vx = GetSystemMetrics(76); // SM_XVIRTUALSCREEN
        int vy = GetSystemMetrics(77); // SM_YVIRTUALSCREEN
        int vw = GetSystemMetrics(78); // SM_CXVIRTUALSCREEN
        int vh = GetSystemMetrics(79); // SM_CYVIRTUALSCREEN
        int vRight = vx + vw;
        int vBottom = vy + vh;
        if (rc.Right <= vx || rc.Left >= vRight || rc.Bottom <= vy || rc.Top >= vBottom) return false;
        return true;
    }

해당 함수는 해당 윈도우가 유효한 사걱형을 가지고 있는지를 판단하는 함수로, 중간에 컴퓨터 전체의 가상 데스크탑의 크기를 가져와서 계산한다.

이 포스트는 저작권자의 CC BY 4.0 라이센스를 따릅니다.