이호원 · airtaxi
Windows를 좋아해서 C#과 .NET을 시작했고,
지금은 WinUI·Uno Platform으로 Windows 앱을 만듭니다.
게임 개발자 — 아크스타
인디 리듬게임 회사에서
게임 서버·클라이언트 개발
공학박사 — 컴퓨터공학
선문대학교 · 논문은 멀티모달 LLM과
RAG 기반 화재 대응 조언 시스템
지금 — 서울 현충원
박사 졸업 후
사회복무요원으로 복무 중
— 그래도 개발은 계속됩니다
Windows를 좋아해서 C#과 .NET을 시작했고,
지금은 WinUI·Uno Platform으로 Windows 앱을 만듭니다.
인디 리듬게임 회사에서
게임 서버·클라이언트 개발
선문대학교 · 논문은 멀티모달 LLM과
RAG 기반 화재 대응 조언 시스템
박사 졸업 후
사회복무요원으로 복무 중
— 그래도 개발은 계속됩니다
전부 실제로 돌아가는 .NET 앱입니다. 오늘은 이걸 만드는 법을 이야기합니다.
오늘의 주인공 — 작업표시줄 호스팅 라이브러리
미디어 위젯 · 라이브러리 검증용 PoC → 스토어 출시
시스템 모니터 — 기존 앱에 위젯 기능을 이식
AI CLI 계정·사용량 관리 — 역시 위젯 이식
이 밖에도 AT-Installer · DeskBorder · LidGuard 등 오픈소스 Windows 앱을 15개 넘게 만들어 공개했습니다.
github.com/airtaxi
"작업표시줄 위에 투명한 창을 얹으면 되잖아?"
WinUI 3 · WPF의
평범한 Window
작업표시줄(Shell_TrayWnd)의
자식 HWND로 입양
SetWindowPos + SetWindowRgn으로
빈 공간에 딱 맞춤
그래서 dotnet add package 한 줄이면 되도록, 라이브러리로 다시 짰습니다 — Deskband11Lib
// 1. 창 스타일 교체: 최상위 팝업 → 자식 창 var style = GetWindowLongPtr(hwnd, GWL_STYLE); style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME); style |= WS_CHILD; SetWindowLongPtr(hwnd, GWL_STYLE, style); // 2. 작업표시줄의 자식으로 입양 SetParent(hwnd, taskbarHwnd); // Shell_TrayWnd // 3. 빈 공간에 배치하고 클리핑 SetWindowPos(hwnd, x, y, width, height, ...); SetWindowRgn(hwnd, CreateRectRgn(0, 0, width, height), true);
XAML 트리도, 입력도, 렌더링도 전부 살아있는 진짜 창 — 스타일 비트만 바뀝니다
Detach 시 원래 스타일과 부모를 복원 — 평범한 창으로 귀환
Windows 11 작업표시줄의 콘텐츠는 XAML 컴포지션 — 자식 HWND가 버튼별로 존재하지 않습니다
시작 버튼·앱 버튼과 겹치지 않는 "빈 공간"을 계산해야 하니까요
HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced TaskbarAl = 0 → 왼쪽 정렬 TaskbarAl = 1 → 중앙 정렬 (기본값) // 값이 없다면? 시작 버튼의 X 좌표로 추론
정렬에 따라 빈 공간이 생기는 위치가 완전히 달라집니다 — 원본이 놓친 부분이었죠.
시작 버튼·앱 버튼·위젯 버튼·알림 영역과는 절대 겹치지 않습니다
ThreadPool에서 실행
느려도 괜찮은 곳으로 격리
측정 결과를 lock으로 보호해
스냅샷으로 보관
캐시만 읽어서 배치 계산
UI 스레드는 대기 없음
$ dotnet add package Deskband11Lib.WinUI
var window = new MainWindow(); var host = new TaskbarContentHost(window, rootElement, new TaskbarContentHostOptions { PreferredWidth = 360, PreferredHeight = 48 }); await host.AttachWhenLayoutReadyAsync(); window.Activate();
두 Facade의 API가 완전히 동일 — Window와 FrameworkElement의 네임스페이스만 다릅니다
폭·높이·배치·모니터·애니메이션 전부 TaskbarContentHostOptions 하나로
Windows App SDK · WinUI 3 Facade
WPF Facade
Facade만 추가하면 확장
Facade가 얇을수록 새 프레임워크 지원이 쉬워집니다 — 엔진은 단 하나
작업표시줄에
창 입양
작업표시줄 HWND
생존 확인
HWND가 교체됨 —
내 창은 이미 파괴됨
TaskbarWindowRecreated
→ 앱이 창을 재생성
host.TaskbarWindowRecreated += async (_, _) => { await RecreateMainWindowAsync(); // 창을 새로 만들어 다시 Attach };
보조 모니터가 분리됐다 재연결되어도 자동으로 원래 작업표시줄로 복귀합니다
폭 380 요청
폭 260 요청
폭 200 요청
Local\Deskband11Lib.SlotRegistry — 최대 64슬롯 · 슬롯마다 HWND · 요청 폭 · 우선순위 · 하트비트 기록
Linear · Sine · Cubic · Expo · Circle… 기본은 CircleOut, Func<double,double>이면 뭐든 OK
애니메이션 타이머를 60FPS 고정 대신 모니터의 실제 주사율에 맞춰 구동
옆 위젯이 등장·퇴장해 자리가 바뀌어도 순간이동 대신 슬라이드 애니메이션
작업표시줄의 일부처럼 보이려면, 움직임도 OS처럼 자연스러워야 합니다
SetParent SetWindowPos FindWindowEx GetWindowLongPtrW SetWindowRgn
PInvoke.SetParent(childHwnd, taskbarHwnd); // 시그니처·마샬링·SafeHandle까지 // Microsoft.Windows.CsWin32가 전부 생성
[GeneratedComInterface] [Guid("30cbe57d-d9d0-452a-ab13-7ac5ac4825ee")] internal partial interface IUIAutomation { IUIAutomationElement GetRootElement(); IUIAutomationElement ElementFromHandle(nint hwnd); // COM vtable 호출 코드를 컴파일 타임에 생성 }
무거운 관리 어셈블리 대신 UIA COM 인터페이스를 직접 — 필요한 멤버만
런타임 마샬링 제거 — 상호 운용이 전부 컴파일 타임에 결정됩니다
런타임에 코드를 만들어내는 고전 COM 마샬러는 AOT의 적 — 소스 생성이 답이었습니다
JIT 없이 네이티브 바이너리 — 부팅 직후 뜨는 위젯에 딱 맞는 특성
항상 떠 있는 앱이니 메모리 발자국이 곧 매너입니다
라이브러리 전체가 AOT 분석 경고 0 — 소비 앱이 안심하고 PublishAot
두 앱 모두 통합 코드는 옵션 몇 개 + AttachWhenLayoutReadyAsync 한 줄이 전부였습니다.
taskkill /f /im explorer.exe
작업표시줄이 죽어도 위젯은 스스로 돌아옵니다
시작 버튼을 중앙 ↔ 왼쪽으로 바꾸면, 위젯이 애니메이션과 함께 새 빈 공간으로 이사합니다
위젯 두 개를 동시에 띄우면 서로 겹치지 않고 공간을 나눠 갖습니다
데모의 신이 노하시면 준비된 영상으로 대체합니다 :)
github.com/airtaxi/Deskband11LibMIT · 샘플 프로젝트 복사로 바로 시작Deskband11Lib.WinUI · Deskband11Lib.WpfBarPlay · CLI Account Switcher전부 이 라이브러리로 만든 실물감사합니다 — 질문 받겠습니다. 이호원 · airtaxi