Как избавиться от кнопки закрытия окна консоли?

У меня есть клиент, который использует какое-то старое, но все еще необходимое 32-битное программное обеспечение, которое работает в окне консоли. Необходимо отключить кнопку «Закрыть», так как закрытие Консоли с помощью этой кнопки вызывает серьезные проблемы в этом программном обеспечении.

Я думал о следующем пути:

1) Найдите дескриптор активной консоли
2) Отключите кнопку закрытия с помощью функции GetSystemMenu

Может быть, я совершенно не прав, но мне пока не удалось найти способ сделать это.

Изменить:

Проблема только в кнопке закрытия. Конечно, пользователи также могут выйти из программы с помощью Alt+F4 или диспетчера задач, но они этого не делают. Они используют кнопку закрытия, поэтому я хочу отключить ее.

Конечно, лучшим решением было бы отключить все способы отмены программы, но и отключение кнопки «Закрыть» сработало бы.

Запуск программы внутри Windows Form также является одним из возможных решений.


person Nils Schuder    schedule 30.05.2020    source источник
comment
Можете ли вы рассмотреть возможность перемещения окна так, чтобы его строка меню находилась за пределами верхней части экрана?   -  person Caius Jard    schedule 30.05.2020
comment
Вы также можете сделать части форм Windows прозрачными, чтобы вы могли использовать приложение форм для запуска этой программы DOS, а затем установить себя AlwaysOnTop, иметь большое прозрачное отверстие в середине формы и располагаться так, чтобы оставшийся контур границы форма окна скрывает контур окна dos...   -  person Caius Jard    schedule 30.05.2020
comment
В вашем вопросе не так много деталей, но я предполагаю, что проблемы возникают, если приложение dos принудительно завершается любым способом, включая диспетчер задач, поэтому вам, возможно, придется приложить значительные усилия, чтобы сделать компьютер, который запускает его в режиме киоска, отключает диспетчер задач и т. д. Нам понадобится больше деталей, чтобы действительно помочь   -  person Caius Jard    schedule 30.05.2020
comment
Вы можете запустить процесс, работающий в консольной подсистеме, и при этом система не должна выделять для него консоль. Передача CREATE_NO_WINDOW в качестве dwCreationFlags при вызове CreateProcess делает это. Без окна консоли вам больше не нужно мешать пользователю взаимодействовать с ним. @cai Нет программы для DOS. В вопросе четко указано, что это 32-битное программное обеспечение.   -  person IInspectable    schedule 31.05.2020
comment
Под программой DOS я имел в виду программу, работающую в окне консоли.   -  person Caius Jard    schedule 31.05.2020


Ответы (1)


Чтобы взаимодействовать с чужим окном, вам нужно сначала найти его/подтвердить его существование.

У нас есть разные способы найти окно. Здесь я рассматриваю FindWindowEx и Process.GetProcessesByName(). Автоматизация пользовательского интерфейса и EnumWindows в конечном итоге предоставляет другие варианты.

Сохраните заголовок окна CMD где-нибудь, например, в поле экземпляра (это могут быть настройки проекта или что-то еще, к чему вы можете получить доступ во время выполнения).

Private cmdWindowTitle As String = "The Window Title"

FindWindowEx более полезен, если вы точно знаете, что такое заголовок окна, и он не меняется со временем.
Process.GetProcessesByName() можно использовать для поиска окна по имени процесса, а затем проверить, является ли Process.MainWindowTitle.Contains() хотя бы частичным известная строка.

Если вместо этого окно консоли принадлежит текущему процессу, вам просто нужно:

Process.GetCurrentProcess().MainWindowHandle

' -- If the Console Window belongs to the current Process: --
Dim cmdWindowHandle = Process.GetCurrentProcess().MainWindowHandle
' -----------------------------------------------------------

' -- Find it when the exact Window title is known: --
Dim cmdWindowHandle As IntPtr = NativeMethods.GetCmdWindowByCaption(cmdWindowTitle)
' -----------------------------------------------------------

' -- Find it when only a partial caption is available: --
Dim cmdWindowHandle As IntPtr = IntPtr.Zero
Dim cmdProc = Process.GetProcessesByName("cmd").
    FirstOrDefault(Function(p) p.MainWindowTitle.Contains(cmdWindowTitle))
If cmdProc IsNot Nothing Then
    cmdWindowHandle = cmdProc.MainWindowHandle
End If
' -----------------------------------------------------------

' Choose one of the above, then, in any case:
If cmdWindowHanle <> IntPtr.Zero Then
    NativeMethods.WindowDisableSysMenu(cmdWindowHandle)
End If

Примечание. Здесь я предполагаю, что имя процесса — cmd, а имя класса окна — ConsoleWindowClass. А может и не быть. Измените их по мере необходимости.

Поскольку теперь в Окне нет кнопок SystemMenu или Закрыть (мы просто спрятали их все), его нельзя закрыть с помощью ALT+F4 или любым другим способом, кроме Диспетчера задач (или дождаться его закрытия < em>естественно).
Чтобы закрыть его из приложения, отправьте WM_CLOSE сообщение:

' -- find the Window as described before --
Dim cmdWindowHandle As IntPtr = NativeMethods.GetCmdWindowByCaption(cmdWindowTitle)
If Not cmdWindowHandle.Equals(IntPtr.Zero) Then
    NativeMethods.SendCloseMessage(cmdWindowHandle)
End If

NativeMethods заявления:

Public Class NativeMethods

    Private Const WM_CLOSE As Integer = &H10

    Public Enum WinStyles As UInteger
        WS_MAXIMIZE = &H1000000
        WS_MAXIMIZEBOX = &H10000
        WS_MINIMIZE = &H20000000
        WS_MINIMIZEBOX = &H20000
        WS_SYSMENU = &H80000
    End Enum

    Public Enum GWL_Flags As Integer
        GWL_STYLE = -16
        GWL_EXSTYLE = -20
    End Enum

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function SendMessage(hWnd As IntPtr, uMsg As WinMessage, wParam As Integer, lParam As Integer) As Integer
    End Function

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function FindWindowEx(hwndParent As IntPtr, hwndChildAfter As IntPtr, lpszClass As String, lpszWindow As String) As IntPtr
    End Function
    
    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function GetWindowLong(hWnd As IntPtr, nIndex As GWL_Flags) As IntPtr
    End Function

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function SetWindowLong(hWnd As IntPtr, nIndex As GWL_Flags, dwNewLong As IntPtr) As IntPtr
    End Function

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function GetWindowLongPtr(hWnd As IntPtr, nIndex As GWL_Flags) As IntPtr
    End Function

    <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Private Shared Function SetWindowLongPtr(hWnd As IntPtr, nIndex As GWL_Flags, dwNewLong As IntPtr) As IntPtr
    End Function

    ' Public wrappers
    Public Shared Function GetWindowLongUni(hWnd As IntPtr, nIndex As GWL_Flags) As Integer
        If IntPtr.Size = 8 Then
            Return GetWindowLongPtr(hWnd, nIndex).ToInt32()
        Else
            Return GetWindowLong(hWnd, nIndex).ToInt32()
        End If
    End Function

    Public Shared Function SetWindowLongUni(hWnd As IntPtr, nIndex As GWL_Flags, dwNewLong As Integer) As Integer
        If IntPtr.Size = 8 Then
            Return SetWindowLongPtr(hWnd, nIndex, New IntPtr(dwNewLong)).ToInt32()
        Else
            Return SetWindowLong(hWnd, nIndex, New IntPtr(dwNewLong)).ToInt32()
        End If
    End Function

    Public Shared Function GetCmdWindowByCaption(cmdCaption As String) As IntPtr
        Return FindWindowEx(IntPtr.Zero, IntPtr.Zero, "ConsoleWindowClass", cmdCaption)
    End Function

    Public Shared Sub WindowDisableSysMenu(windowHandle As IntPtr)
        Dim styles As Integer = GetWindowLongUni(windowHandle, GWL_Flags.GWL_STYLE)
        styles = styles And Not CInt(WinStyles.WS_SYSMENU)
        SetWindowLongUni(windowHandle, GWL_Flags.GWL_STYLE, styles)
    End Sub

    Public Shared Sub SendCloseMessage(windowHandle As IntPtr)
        SendMessage(windowHandle, WM_CLOSE, 0, 0)
    End Sub
End Class
person Jimi    schedule 30.05.2020
comment
Спасибо, Джими, но вы предполагаете, что нет системного меню и кнопки закрытия. Но есть кнопка закрытия, и я хочу хотя бы отключить X-кнопку. Изображение стандартного dosbox с заголовком и SystemMenu можно найти здесь: ссылка - person Nils Schuder; 30.05.2020
comment
Я предполагаю прямо противоположное. Я предполагаю, что в окне ЕСТЬ как кнопка «Системное меню», так и кнопка «Закрыть». Этот код отключает их все (заставляя эти кнопки исчезать) - person Jimi; 30.05.2020
comment
Извини меня, пожалуйста, Джими. Тогда я вас неправильно понял - может быть из-за проблемы с переводом. Попробую ваш код... Спасибо. - person Nils Schuder; 30.05.2020
comment
Нет, вы правы, я исправил это предложение, потому что оно было неясным. Теперь он должен лучше описывать намерения и ожидания кода. Я также немного изменил код, теперь вызовы методов стали более краткими. - person Jimi; 30.05.2020
comment
Я попробовал ваш код. Я скопировал его в новый консольный проект и собрал exe. Ошибки не было. Затем я запускаю exe в новом DOSBox с заголовком Eingabeaufforderung и, конечно же, я также изменил заголовок Windows на Eingabeaufforderung, чтобы он совпадал. Во время выполнения также нет ошибок, но и никаких изменений. Кнопка закрытия все еще там и работает. Если я нажму на нее, MOSDOS-Box все равно сразу же закроется. - person Nils Schuder; 30.05.2020
comment
Вы пробовали оба метода, чтобы получить дескриптор окна? И с GetCmdWindowByCaption, и с GetProcessesByName? Вы проверили, действительно ли один из них может получить дескриптор этого окна? Вы уверены, что заголовок окна просто Eingabeaufforderung, а имя класса ConsoleWindowClass (как описано в примечании, это может быть не так)? Вы должны отлаживать свой код. Поставьте точку останова в первой строке, переходите к строке за строкой и проверяйте полученные значения. Этот код не генерируется, если дескриптор не найден (если вы его не сделаете). - person Jimi; 30.05.2020
comment
Я убедился, что Eingabeaufforderung верен (как вы можете видеть в моей ссылке несколькими сообщениями ранее). Я пока пробовал только GetCmdWindowByCaption. Может быть, ConsoleWindowClass неверен. Я добавил строку Console.WriteLine(cmdWindowHandle), и эта строка возвращает 0. Я предполагаю, что это означает, что дескриптор не найден. Затем я попробовал следующее: Публичная общая функция GetCmdWindowByCaption (cmdCaption As String) As IntPtr Return FindWindowEx (IntPtr.Zero, IntPtr.Zero, vbNullString, vbNullString) End Function Это возвращает всегда 67176. Я думаю, что это может быть любое окно сейчас? - person Nils Schuder; 30.05.2020
comment
После этого отображается ошибка: Unbehandelte Ausnahme: System.OverflowException: Die arithmetische Operation hat einen Überlauf verursacht. в Xweg.Module1.NativeMethods.GetWindowLongUni(IntPtr hWnd, GWL_Flags nIndex) в Xweg.Module1.Main() - person Nils Schuder; 30.05.2020
comment
Я попытался найти запущенные процессы на моей машине с помощью GetProcesses, и этот код возвращает мне только один процесс с ProcessName = cmd и ProcessID = 6208. Если я запускаю другой DOSBOX, я получаю два элемента: ProcessName = cmd и ProcessID = 6208 ProcessName = cmd и ProcessID = 29888 Может ли это помочь? - person Nils Schuder; 30.05.2020
comment
Я думаю, что мне наконец удалось это с помощью ThisProcess.MainWindowHandle, найденного GetProcesses. Полного блока управления с кнопками «Свернуть», «Развернуть» и «Закрыть» больше нет. - person Nils Schuder; 30.05.2020
comment
Исключение переполнения, которое вы получаете с помощью GetWindowLongUni, вероятно, вызвано приложением, скомпилированным как x86, работающим в системе x64 (что в любом случае странно, поскольку этот метод создан именно для того, чтобы избежать этого - если только вы не передаете неправильный дескриптор или IntPtr .Нуль). Если по какой-либо причине нет необходимости запускать процесс как 32-битный, скомпилируйте его с использованием профиля AnyCPU и снимите флажок «Предпочитать 32-битный» на панели «Проект Properties-> Compile». Кстати, в этом коде, закомментированном, у вас уже есть способ Process для получения дескриптора окна cmd. - person Jimi; 30.05.2020
comment
Спасибо, Джими, я больше не получаю исключений. Мне удалось найти дескриптор активных окон. Это ооочень просто: Private Declare Function GetForegroundWindow Lib "user32" Alias "GetForegroundWindow" () As IntPtr Dim cmdWindowHandle As IntPtr = GetForegroundWindow If cmdWindowHandle <> IntPtr.Zero Then NativeMethods.WindowDisableSysMenu(cmdWindowHandle) End If - person Nils Schuder; 30.05.2020
comment
GetForegroundWindow - неправильный путь. Вы заметите достаточно скоро, без необходимости объяснять, почему. Используйте код, который я разместил. - person Jimi; 30.05.2020
comment
Хорошо, я могу использовать «GetProcesses» и следующий цикл через все найденные процессы, чтобы найти дескриптор. Можно отфильтровать найденные процессы по ProcessName (cmd) и по ProcessMainTitle, чтобы найти нужный. Конечно, я еще раз взгляну на ваш код с «Process.GetProcessesByName()». - person Nils Schuder; 31.05.2020
comment
Предоставленный код в обоих вариантах уже делает это. Если вы объясните, что у вас за проблема в идентификации окна консоли, я могу предложить решение. Обратите внимание, что этот код работает в Windows 7, Windows 8.1 и Windows 10 со стандартным окном консоли. Если имя вашего класса окна консоли не ConsoleWindowClass, определите правильное имя класса и замените значение в методе GetCmdWindowByCaption. - person Jimi; 31.05.2020
comment
Как я описал выше, 'FindWindowEx' не находит дескриптор и возвращает 0. Я убедился, что заголовок правильный. Окно моей консоли всегда является только что открытым стандартным окном консоли, в котором до сих пор не запущена ни одна программа. Я дал ссылку на картинку выше. Окно консоли, которое я ищу, всегда открыто. Я хочу запустить свою программу, которая отключает кнопку закрытия, как первую программу в этом окне консоли, а затем старую программу, которая требует отключения кнопки закрытия. - person Nils Schuder; 31.05.2020
comment
Если FindWindowEx не находит это Window, это происходит потому, что 1) указанный вами заголовок неверен (не может быть частичным и зависит от регистра) 2) неправильное имя класса Window. Это то, что вам необходимо проверить и адаптировать, как уже было описано. Если по какой-то причине вы не можете адаптировать эти значения, используйте метод Process, который уже опубликован и закомментирован в том же фрагменте кода. - person Jimi; 31.05.2020
comment
У меня возникла мысль... Может между нами есть важное недопонимание в одном из первых постов, что усложняет дело как надо. Вы написали: чтобы взаимодействовать с чужим окном, вам нужно сначала найти его/убедиться, что оно существует. Но винда не чужая - это та, в которой работает или должна работать сама программа. Мой инструмент для отключения кнопки закрытия всегда запускается в том же окне, где кнопка закрытия должна быть отключена. - person Nils Schuder; 01.06.2020
comment
Ну, это адское недоразумение. В этом случае вам просто нужно Process.GetCurrentProcess().MainWindowHandle. Я обновил код и примечания в ответе. - person Jimi; 01.06.2020
comment
@NilsSchuder Если этот ответ решает вашу проблему, вы можете принять его, чтобы сделать этот вопрос и ответить более понятным. :) - person Rita Han; 02.06.2020