메인 콘텐츠로 이동하기
  1. Dev-Logs/

윈도우 창 제목 변경 프로그램 (Window Title Manager, AutoHotkey v2)

·12 분

지금 실행 중인 창의 목록을 보여주고, 그중 하나를 골라 창 제목(타이틀바에 보이는 글자)을 원하는 이름으로 바꿔 주는 작은 프로그램의 코드를 공개하는 글입니다. AutoHotkey v2로 만들었고, AI와 함께 작업하며 다듬었습니다.

같은 프로그램 창을 여러 개 띄워 놓고 구분하고 싶을 때(예: 같은 이름의 창 여러 개에 D1, D2처럼 이름 붙이기), 또는 작업표시줄·캡처 화면에서 창 이름을 알아보기 쉽게 바꾸고 싶을 때 쓸 수 있습니다.

이 글의 구성 방침: 실행에 필요한 폴더 구조와 소스 파일을 빠짐없이 올렸습니다. 각 파일은 오른쪽 목차에서 클릭해서 바로 이동할 수 있습니다. 백업본(.bak, .OLD)과 실행 중 저장되는 기록 파일(TitleHistory.txt)은 본문에서 뺐습니다.

🟡 프로젝트 소개 #

🟡 어떤 프로그램인가 #

  • 열려 있는 창을 창 제목 · 프로세스 · HWND 세 칸으로 목록에 보여줍니다. 1초마다 자동으로 갱신됩니다.
  • 새로 뜬 창은 목록 맨 위에 올라오고(프로세스가 가장 늦게 시작된 창이 위), 닫힌 창은 목록에서 사라집니다. 바뀐 부분만 반영해서 깜빡임과 선택 해제가 없습니다.
  • 목록에서 창을 더블클릭하거나 [제목 변경] 버튼을 누르면 새 제목 입력창이 뜹니다.
  • 입력창 아래에는 최근 사용한 제목 20개가 나오고, 하나를 더블클릭하면 바로 적용됩니다. 기록은 TitleHistory.txt에 저장되어 프로그램을 껐다 켜도 남습니다.
  • 이 프로그램 자신의 창과 화면에 보이지 않는 유령 창(UWP 숨김 창), 바탕화면(Program Manager)은 목록에서 제외합니다.

🟡 사용 방법 #

  1. AutoHotkey v2를 설치합니다. (#Requires AutoHotkey v2.0)
  2. 아래 폴더 구조대로 파일을 두고 Main.ahk를 실행합니다.
  3. 목록에서 제목을 바꿀 창을 더블클릭하고, 새 제목을 입력한 뒤 확인을 누릅니다.
  4. 종료는 [종료] 버튼이나 창의 X 버튼입니다.

대상 프로그램이 관리자 권한으로 실행 중이면 제목 변경이 실패합니다. 이때는 이 스크립트도 관리자 권한으로 실행해야 합니다. (실패하면 프로그램이 같은 안내 메시지를 띄웁니다.)

🟡 폴더 구조 #

script/
├─ Classes/
│  ├─ Monitor.ahk             # 특정 프로그램 창 제목을 유지하려던 클래스 (미완성, 현재 미사용)
│  ├─ Monitor.ahk.bak         # [본문 제외] 백업본
│  ├─ TitleChanger.ahk        # 제목 변경 + 원본 제목 저장·복원
│  ├─ TitleChanger.ahk.bak    # [본문 제외] 백업본
│  ├─ WinAPI.ahk              # Windows API 호출 모음 (제목 읽기·쓰기, PID, 클래스명 등)
│  ├─ WinAPI.ahk.bak          # [본문 제외] 백업본
│  ├─ WindowManager.ahk       # 창 목록 수집·검색 (현재 미사용)
│  ├─ WindowManager.ahk.bak   # [본문 제외] 백업본
│  ├─ WindowSelector.ahk      # 메인 화면 (창 목록 · 제목 입력창 · 기록 파일)
│  └─ WindowSelector.ahk.bak  # [본문 제외] 백업본
├─ Main.ahk                   # 시작 파일 (창 하나 띄우고 계속 실행)
├─ Main.ahk,bak               # [본문 제외] 백업본
├─ Main.ahk.OLD               # [본문 제외] 백업본
├─ TitleHistory.txt           # [본문 제외] 실행 중 저장되는 기록
├─ winex.ps1                  # 로그온/시작 시 실행되는 작업 스케줄러 항목 조회
├─ wintest.ps1                # 시작 프로그램·서비스 종합 점검 (조회 전용)
└─ wintestSC.bat              # wintest.ps1을 실행하는 배치 파일

🟡 동작 구조 #

Main.ahk
   └─ WindowSelector.Show()      메인 창 · 1초 타이머로 목록 갱신 · 제목 입력창 · 기록 파일
         ├─ WinAPI               제목 읽기(GetWindowTextW) · 프로세스 ID · 클래스명 등 Windows API 호출
         └─ TitleChanger         제목 변경(SetWindowTextW) + 처음 제목 저장/복원

TitleChanger에는 원래 제목으로 되돌리는 Restore, RestoreAll 함수도 들어 있지만, 현재 화면에서는 아직 연결하지 않았습니다.

복사해서 쓸 때 주의: 코드 안의 한글(주석과 화면 문구)이 깨지지 않게 파일은 UTF-8(BOM 포함) 으로 저장하세요. AutoHotkey v2는 BOM이 없는 파일을 UTF-8로 읽습니다.

🟣 진입점 — 시작 파일 #

🟣 Main.ahk — 시작 파일 (창 하나 띄우고 계속 실행) #

#Requires AutoHotkey v2.0
#SingleInstance Force

#Include Classes\WinAPI.ahk
#Include Classes\TitleChanger.ahk
#Include Classes\WindowSelector.ahk

; 창을 띄우고 계속 실행 (종료 버튼 또는 창 닫기(X)로 종료)
WindowSelector.Show()

🔴 Classes — 핵심 코드 #

🔴 Classes/WindowSelector.ahk — 메인 화면 (창 목록 · 제목 입력창 · 기록 파일) #

class WindowSelector
{
    static MainGui   := ""
    static LV        := ""
    static Status    := ""
    static BtnChange := ""
    static BtnRefresh := ""
    static BtnExit   := ""
    static LastSnap  := ""

    ; 변경했던 제목 기록 (Main.ahk와 같은 폴더, 최근 것이 위)
    static HistoryFile := A_ScriptDir "\TitleHistory.txt"
    static HistoryMax  := 20

    ;==================================================
    ; 메인 창 표시 (상시 실행)
    ;==================================================
    static Show()
    {
        g := Gui("+Resize +MinSize560x300", "Window Title Manager")
        g.SetFont("s10", "맑은 고딕")
        this.MainGui := g

        g.AddText("w700", "제목을 바꿀 창을 선택 후 더블클릭 (또는 [제목 변경] 버튼)")

        this.LV := g.AddListView("w700 h320 Grid -Multi NoSort", ["창 제목", "프로세스", "HWND"])
        this.LV.ModifyCol(1, 400)
        this.LV.ModifyCol(2, 160)
        this.LV.ModifyCol(3, 100)

        this.BtnChange  := g.AddButton("w110 h30", "제목 변경")
        this.BtnRefresh := g.AddButton("x+10 w110 h30", "새로고침")
        this.BtnExit    := g.AddButton("x+10 w110 h30", "종료")
        this.Status     := g.AddText("x+20 yp w250 h30 0x200", "")

        this.LV.OnEvent("DoubleClick", ObjBindMethod(this, "ChangeSelected"))
        this.BtnChange.OnEvent("Click", ObjBindMethod(this, "ChangeSelected"))
        this.BtnRefresh.OnEvent("Click", ObjBindMethod(this, "Refresh", true))
        this.BtnExit.OnEvent("Click", (*) => ExitApp())

        g.OnEvent("Size", ObjBindMethod(this, "OnSize"))
        g.OnEvent("Close", (*) => ExitApp())

        this.Refresh(true)
        g.Show("w720 h440")

        ; 1초마다 자동 갱신 (새 창 실행/종료/제목 변경이 리스트에 반영됨)
        SetTimer(ObjBindMethod(this, "Refresh"), 1000)
    }

    ;==================================================
    ; 창 크기 변경 시 레이아웃 조정
    ;==================================================
    static OnSize(guiObj, minMax, w, h)
    {
        if (minMax = -1)
            return

        this.LV.Move(, , w - 20, h - 100)
        this.BtnChange.Move(, h - 45)
        this.BtnRefresh.Move(, h - 45)
        this.BtnExit.Move(, h - 45)
        this.Status.Move(, h - 45)
    }

    ;==================================================
    ; 창 리스트 갱신 (변경된 부분만 반영 -> 깜빡임/선택 해제 없음)
    ;==================================================
    static Refresh(force := false, *)
    {
        cur := Map()
        myPid := DllCall("GetCurrentProcessId", "uint")

        for hwnd in WinGetList()
        {
            title := WinAPI.GetTitle(hwnd)

            if (title = "" || title = "Program Manager")
                continue

            if this.IsCloaked(hwnd)
                continue

            try
            {
                pid := WinGetPID(hwnd)
            }
            catch
            {
                continue
            }

            ; 이 프로그램 자신의 창은 제외
            if (pid = myPid)
                continue

            try
            {
                exe := WinGetProcessName(hwnd)
            }
            catch
            {
                exe := ""
            }

            cur[hwnd] := [title, exe, pid]
        }

        ; 이전과 똑같으면 아무것도 안 함
        snap := ""
        for hwnd, info in cur
            snap .= hwnd "|" info[1] "|" info[2] "`n"

        if (!force && snap = this.LastSnap)
            return

        this.LastSnap := snap

        lv := this.LV
        lv.Opt("-Redraw")

        ; 1) 기존 행: 사라진 창은 삭제, 제목/프로세스가 바뀐 창은 수정
        seen := Map()
        row := lv.GetCount()
        while (row >= 1)
        {
            h := Integer(lv.GetText(row, 3))

            if !cur.Has(h)
            {
                lv.Delete(row)
            }
            else
            {
                info := cur[h]

                if (lv.GetText(row, 1) != info[1] || lv.GetText(row, 2) != info[2])
                    lv.Modify(row, "", info[1], info[2])

                seen[h] := true
            }

            row--
        }

        ; 2) 새로 생긴 창은 맨 위에 추가 (프로세스 시작 시각 오름차순으로 위에 쌓음
        ;    -> 같은 순간에 여러 개가 떠도 가장 최근에 실행된 것이 맨 위)
        lines := ""
        for h, info in cur
        {
            if !seen.Has(h)
                lines .= Format("{:020d}", this.StartTime(info[3])) "|" h "`n"
        }

        if (lines != "")
        {
            lines := Sort(Trim(lines, "`n"))

            Loop Parse, lines, "`n"
            {
                parts := StrSplit(A_LoopField, "|")
                h := Integer(parts[2])
                info := cur[h]
                lv.Insert(1, "", info[1], info[2], h)
            }
        }

        lv.Opt("+Redraw")

        this.Status.Text := "창 " lv.GetCount() "개 (1초마다 자동 갱신)"
    }

    ;==================================================
    ; 프로세스 시작 시각 (FILETIME 정수, 못 얻으면 0)
    ;==================================================
    static StartTime(pid)
    {
        hProc := DllCall("OpenProcess"
            , "uint", 0x1000
            , "int", 0
            , "uint", pid
            , "ptr")

        if !hProc
            return 0

        buf := Buffer(32, 0)

        ok := DllCall("GetProcessTimes"
            , "ptr", hProc
            , "ptr", buf.Ptr
            , "ptr", buf.Ptr + 8
            , "ptr", buf.Ptr + 16
            , "ptr", buf.Ptr + 24
            , "int")

        DllCall("CloseHandle", "ptr", hProc)

        return ok ? NumGet(buf, 0, "int64") : 0
    }

    ;==================================================
    ; DWM에 의해 숨겨진(cloaked) 창인지 확인 (UWP 유령 창 제외용)
    ;==================================================
    static IsCloaked(hwnd)
    {
        cloaked := 0

        hr := DllCall("dwmapi\DwmGetWindowAttribute"
            , "ptr", hwnd
            , "uint", 14
            , "uint*", &cloaked
            , "uint", 4
            , "int")

        return (hr = 0 && cloaked != 0)
    }

    ;==================================================
    ; 선택한 창의 제목 변경 시작 (더블클릭 / 버튼 공용)
    ;==================================================
    static ChangeSelected(*)
    {
        row := this.LV.GetNext()

        if !row
        {
            MsgBox("먼저 창을 선택하세요.", "Title 변경", "Owner" this.MainGui.Hwnd)
            return
        }

        hwnd := Integer(this.LV.GetText(row, 3))
        this.AskTitle(hwnd)
    }

    ;==================================================
    ; 새 제목 입력창 (메인 창은 그대로 유지)
    ;==================================================
    static AskTitle(hwnd)
    {
        if !WinAPI.IsWindow(hwnd)
        {
            MsgBox("이 창은 이미 닫혔습니다.", "Title 변경", "Owner" this.MainGui.Hwnd)
            this.Refresh(true)
            return
        }

        history := this.LoadHistory()

        dlg := Gui("+Owner" this.MainGui.Hwnd, "Title 변경")
        dlg.SetFont("s10", "맑은 고딕")
        dlg.AddText("w460", "새로운 제목 입력")

        edit := dlg.AddEdit("w460", WinAPI.GetTitle(hwnd))

        if (history.Length > 0)
        {
            dlg.AddText("w460", "최근 사용한 제목 (더블클릭하면 바로 적용)")

            lb := dlg.AddListBox("w460 r10", history)
            lb.OnEvent("Change", ObjBindMethod(this, "PickHistory", edit))
            lb.OnEvent("DoubleClick", ObjBindMethod(this, "PickAndApply", dlg, edit, hwnd))
        }

        btnOk  := dlg.AddButton("Default w100", "확인")
        btnCan := dlg.AddButton("x+10 w100", "취소")

        btnOk.OnEvent("Click", ObjBindMethod(this, "ApplyTitle", dlg, edit, hwnd))
        btnCan.OnEvent("Click", ObjBindMethod(this, "CloseDialog", dlg))
        dlg.OnEvent("Close", ObjBindMethod(this, "CloseDialog", dlg))
        dlg.OnEvent("Escape", ObjBindMethod(this, "CloseDialog", dlg))

        ; 입력창이 떠 있는 동안 메인 창 조작 잠금
        this.MainGui.Opt("+Disabled")
        dlg.Show()
        edit.Focus()
    }

    ;==================================================
    ; 확인 버튼: 제목 적용
    ;==================================================
    static ApplyTitle(dlg, edit, hwnd, *)
    {
        newTitle := edit.Value
        this.CloseDialog(dlg)

        if (newTitle != "")
        {
            if !TitleChanger.SetTitle(hwnd, newTitle)
            {
                MsgBox(
                    "제목 변경에 실패했습니다.`n`n"
                    "대상이 관리자 권한으로 실행 중인 프로그램이면`n"
                    "이 스크립트도 관리자 권한으로 실행해야 합니다.",
                    "Title 변경",
                    "Owner" this.MainGui.Hwnd
                )
            }
            else
            {
                this.SaveHistory(newTitle)
            }
        }

        this.Refresh(true)
    }

    ;==================================================
    ; 입력창 닫기 (메인 창 잠금 해제 후 제거)
    ;==================================================
    static CloseDialog(dlg, *)
    {
        this.MainGui.Opt("-Disabled")
        dlg.Destroy()
    }

    ;==================================================
    ; 기록 리스트에서 선택 -> 입력칸에 채우기
    ;==================================================
    static PickHistory(edit, lb, *)
    {
        if (lb.Text != "")
            edit.Value := lb.Text
    }

    ;==================================================
    ; 기록 리스트에서 더블클릭 -> 바로 적용
    ;==================================================
    static PickAndApply(dlg, edit, hwnd, lb, *)
    {
        if (lb.Text = "")
            return

        edit.Value := lb.Text
        this.ApplyTitle(dlg, edit, hwnd)
    }

    ;==================================================
    ; 기록 파일 읽기 (한 줄에 제목 하나, 최근 것이 위)
    ;==================================================
    static LoadHistory()
    {
        list := []

        if !FileExist(this.HistoryFile)
            return list

        try
        {
            text := FileRead(this.HistoryFile, "UTF-8")
        }
        catch
        {
            return list
        }

        for line in StrSplit(text, "`n", "`r")
        {
            if (line != "" && list.Length < this.HistoryMax)
                list.Push(line)
        }

        return list
    }

    ;==================================================
    ; 기록 파일에 저장 (중복은 맨 위로, 최대 20개)
    ;==================================================
    static SaveHistory(title)
    {
        list := [title]

        for t in this.LoadHistory()
        {
            if (t !== title && list.Length < this.HistoryMax)
                list.Push(t)
        }

        text := ""
        for t in list
            text .= t "`r`n"

        try
        {
            f := FileOpen(this.HistoryFile, "w", "UTF-8")
            f.Write(text)
            f.Close()
        }
    }
}

🔴 Classes/TitleChanger.ahk — 제목 변경 + 원본 제목 저장·복원 #

class TitleChanger
{
    ; 원본 제목 저장
    static OriginalTitles := Map()


    ;==================================================
    ; 제목 변경
    ;==================================================
    static SetTitle(hwnd, newTitle)
    {
        if !WinAPI.IsWindow(hwnd)
            return false


        ; 최초 1회만 원본 저장
        if !this.OriginalTitles.Has(hwnd)
        {
            oldTitle := WinAPI.GetTitle(hwnd)

            this.OriginalTitles[hwnd] := oldTitle
        }


        return WinAPI.SetTitle(hwnd, newTitle)
    }



    ;==================================================
    ; 원본 제목 복원
    ;==================================================
    static Restore(hwnd)
    {
        if !this.OriginalTitles.Has(hwnd)
            return false


        oldTitle := this.OriginalTitles[hwnd]


        result := WinAPI.SetTitle(hwnd, oldTitle)


        if result
        {
            this.OriginalTitles.Delete(hwnd)
        }


        return result
    }



    ;==================================================
    ; 모든 창 복원
    ;==================================================
    static RestoreAll()
    {
        for hwnd, oldTitle in this.OriginalTitles
        {
            if WinAPI.IsWindow(hwnd)
            {
                WinAPI.SetTitle(hwnd, oldTitle)
            }
        }


        this.OriginalTitles.Clear()
    }



    ;==================================================
    ; 저장된 원본 제목 가져오기
    ;==================================================
    static GetOriginal(hwnd)
    {
        if this.OriginalTitles.Has(hwnd)
            return this.OriginalTitles[hwnd]


        return ""
    }



    ;==================================================
    ; 변경 여부 확인
    ;==================================================
    static IsChanged(hwnd)
    {
        return this.OriginalTitles.Has(hwnd)
    }



    ;==================================================
    ; 저장 정보 삭제
    ;==================================================
    static Remove(hwnd)
    {
        if this.OriginalTitles.Has(hwnd)
            this.OriginalTitles.Delete(hwnd)
    }

}

🔴 Classes/WinAPI.ahk — Windows API 호출 모음 (제목 읽기·쓰기, PID, 클래스명 등) #

class WinAPI
{
    ;--------------------------------------------------
    ; HWND의 현재 제목 얻기
    ;--------------------------------------------------
    static GetTitle(hwnd)
    {
        len := DllCall("GetWindowTextLengthW"
            , "ptr", hwnd
            , "int")

        buf := Buffer((len + 1) * 2)

        DllCall("GetWindowTextW"
            , "ptr", hwnd
            , "ptr", buf.Ptr
            , "int", len + 1)

        return StrGet(buf)
    }

    ;--------------------------------------------------
    ; 제목 변경
    ;--------------------------------------------------
    static SetTitle(hwnd, title)
    {
        result := DllCall(
            "SetWindowTextW",
            "ptr", hwnd,
            "str", title,
            "int"
            )


               Return result
    }

    ;--------------------------------------------------
    ; 창 존재 여부
    ;--------------------------------------------------
    static IsWindow(hwnd)
    {
        return DllCall("IsWindow"
            , "ptr", hwnd
            , "int")
    }

    ;--------------------------------------------------
    ; 창 표시 여부
    ;--------------------------------------------------
    static IsVisible(hwnd)
    {
        return DllCall("IsWindowVisible"
            , "ptr", hwnd
            , "int")
    }

    ;--------------------------------------------------
    ; 프로세스 ID 얻기
    ;--------------------------------------------------
    static GetPID(hwnd)
    {
        pid := 0

        DllCall("GetWindowThreadProcessId"
            , "ptr", hwnd
            , "uint*", &pid)

        return pid
    }

    ;--------------------------------------------------
    ; 프로세스 이름
    ;--------------------------------------------------
    static GetProcessName(hwnd)
    {
        try
        {
            pid := this.GetPID(hwnd)
            return ProcessGetName(pid)
        }
        catch
        {
            return ""
        }
    }

    ;--------------------------------------------------
    ; 클래스명
    ;--------------------------------------------------
    static GetClass(hwnd)
    {
        buf := Buffer(512)

        DllCall("GetClassNameW"
            , "ptr", hwnd
            , "ptr", buf.Ptr
            , "int", 256)

        return StrGet(buf)
    }
}

🔵 Classes — 현재 사용하지 않는 클래스 #

Main.ahk가 불러오지 않는 파일들입니다. 이전 시도의 코드이고, 특히 Monitor.ahk는 미완성 상태입니다.

🔵 Classes/WindowManager.ahk — 창 목록 수집·검색 (현재 미사용) #

class WindowManager
{
    static List := Map()


    ;==================================================
    ; 모든 창 검색
    ;==================================================
    static Refresh()
    {
        this.List.Clear()

        cb := CallbackCreate(EnumWindowsProc)

        DllCall("EnumWindows"
            , "ptr", cb
            , "ptr", 0)

        CallbackFree(cb)
    }


    ;==================================================
    ; HWND로 정보 가져오기
    ;==================================================
    static Get(hwnd)
    {
        if this.List.Has(hwnd)
            return this.List[hwnd]

        return false
    }


    ;==================================================
    ; HWND 존재 여부
    ;==================================================
    static Exists(hwnd)
    {
        return this.List.Has(hwnd)
    }


    ;==================================================
    ; 제목 검색
    ;==================================================
    static FindByTitle(keyword)
    {
        result := []

        for hwnd, info in this.List
        {
            if InStr(info["Title"], keyword)
                result.Push(info)
        }

        return result
    }


    ;==================================================
    ; 실행 파일 검색
    ;==================================================
    static FindByExe(exe)
    {
        result := []

        for hwnd, info in this.List
        {
            if (StrLower(info["Process"]) = StrLower(exe))
                result.Push(info)
        }

        return result
    }


    ;==================================================
    ; PID 검색
    ;==================================================
    static FindByPID(pid)
    {
        for hwnd, info in this.List
        {
            if (info["PID"] = pid)
                return info
        }

        return false
    }


    ;==================================================
    ; Window Class 검색
    ;==================================================
    static FindByClass(className)
    {
        result := []

        for hwnd, info in this.List
        {
            if (info["Class"] = className)
                result.Push(info)
        }

        return result
    }
}



;======================================================
; EnumWindows Callback
;======================================================
EnumWindowsProc(hwnd, lParam)
{
    if !WinAPI.IsWindow(hwnd)
        return true


    if !WinAPI.IsVisible(hwnd)
        return true


    title := WinAPI.GetTitle(hwnd)


    if (title = "")
        return true


    ; 바탕화면 제외
    if (title = "Program Manager")
        return true



    info := Map()

    info["HWND"] := hwnd
    info["Title"] := title
    info["PID"] := WinAPI.GetPID(hwnd)
    info["Process"] := WinAPI.GetProcessName(hwnd)
    info["Class"] := WinAPI.GetClass(hwnd)



    WindowManager.List[hwnd] := info


    return true
}

🔵 Classes/Monitor.ahk — 특정 프로그램 창 제목을 유지하려던 클래스 (미완성, 현재 미사용) #

class Monitor
{
    static Targets := Map()
	static ProcessTargets := Map()
    __New()
    {
        this.ProcessTargets := Map(
            "D2R.exe", "Diablo II: Resurrected"
        )

        this.Targets := Map()
    }

	;==================================================
	; 프로세스 검사
	;==================================================
	static CheckProcess()
	{
		for exeName, wantedTitle in this.ProcessTargets
		{

			hwnd := WinExist("ahk_exe " exeName)

			if hwnd
			{
				; 이미 등록되어 있으면 생략
				if !this.Targets.Has(hwnd)
				{
					this.Targets[hwnd] := wantedTitle

					; 새 창 발견
					this.ShowWindow(hwnd, wantedTitle)

					TitleChanger.SetTitle(hwnd, wantedTitle)
				}
			}

		}
	}

    ShowWindow(hwnd, wantedTitle)
    {
        current := WinAPI.GetTitle(hwnd)

        MsgBox(
            "발견`n`n"
            "HWND : " hwnd "`n"
            "현재 제목 : " current "`n"
            "변경 제목 : " wantedTitle
        )

        TitleChanger.SetTitle(hwnd, wantedTitle)
    }

    ;==================================================
    ; 전체 검사
    ;==================================================
    static Check()
    {
		this.CheckProcess()

        for hwnd, wantedTitle in this.Targets
        {

            if !WinAPI.IsWindow(hwnd)
                continue

			; 현재 제목 확인
			current := WinAPI.GetTitle(hwnd)

			if (current != wantedTitle)
				; 제목 변경 요청
				TitleChanger.SetTitle(hwnd, wantedTitle)
		}

    }

}

🟢 부록 — 함께 있던 시작 프로그램 점검 스크립트 #

창 제목 프로그램과는 별개로 같은 폴더에 있던 PowerShell 스크립트입니다. 시작 시 자동 실행되는 항목을 조회만 하고 아무것도 바꾸지 않습니다. wintestSC.bat 안의 경로(C:\script\wintest.ps1)는 본인 환경에 맞게 고쳐서 쓰세요.

🟢 wintest.ps1 — 시작 프로그램·서비스 종합 점검 (조회 전용) #

Write-Host "==== [1] HKCU Run ====" -ForegroundColor Cyan
Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" |
Select-Object *

Write-Host "`n==== [2] HKLM Run ====" -ForegroundColor Cyan
Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run" |
Select-Object *

Write-Host "`n==== [3] Startup Folder (User) ====" -ForegroundColor Cyan
Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"

Write-Host "`n==== [4] Startup Folder (All Users) ====" -ForegroundColor Cyan
Get-ChildItem "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"

Write-Host "`n==== [5] Scheduled Tasks (Logon/Startup) ====" -ForegroundColor Cyan
Get-ScheduledTask | Where-Object {
    $_.Triggers -match "AtLogOn|AtStartup"
} | Select-Object TaskName, TaskPath, State

Write-Host "`n==== [6] Automatic Services ====" -ForegroundColor Cyan
Get-Service | Where-Object {$_.StartType -eq "Automatic"} |
Select-Object Name, DisplayName, Status

🟢 winex.ps1 — 로그온/시작 시 실행되는 작업 스케줄러 항목 조회 #

Get-ScheduledTask |
Where-Object {
    $_.Triggers -match "AtLogOn|AtStartup" -and
    $_.TaskPath -notmatch "Microsoft"
} |
Select TaskName, TaskPath, State

🟢 wintestSC.bat — wintest.ps1을 실행하는 배치 파일 #

@echo off
powershell.exe -ExecutionPolicy Bypass -File "C:\script\wintest.ps1"
pause