My goal is to run a third-party application though the cmd shell. My VB program will start multiple instances and I like to set the cmd title to keep track of those multiple windows. I'm running into the following issue: when I change the title using VB, the change is not consistent. The new title is changed back to the default title, as soon as you use a copy/paste function in this window or click anywhere in the cmd window. Here is the VB code I use:
Imports System.Threading
Public Class Form1
Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (ByVal hwnd As Integer, ByVal lpString As String) As Integer
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim h_wnd As Integer
Dim proc As New Process
proc = Process.Start("cmd.exe")
Thread.Sleep(2000)
h_wnd = proc.MainWindowHandle
SetWindowText(h_wnd, "Test Text")
End Sub
End Class
When I do the same thing through PowerShell, the rename is consistent. Here's the PS code I use
Add-Type -Type @"
using System;
using System.Runtime.InteropServices;
namespace WT {
public class Temp {
[DllImport("user32.dll")]
public static extern bool SetWindowText(IntPtr hWnd, string lpString);
}
}
"@
$titletext = "Test Text"
# Start a thread job to change the window title to $titletext
$null = Start-ThreadJob { param( $rawUI, $windowTitle )
Start-Sleep -s 2
if ( $rawUI.WindowTitle -ne $windowTitle ) {
$rawUI.WindowTitle = $windowTitle
}
}-ArgumentList $host.ui.RawUI, $titletext
echo $rawUI
& 'C:\Windows\System32\cmd.exe'
The problem is that I won't be able to use PowerShell, because part of the parameters parsed to the script is a password and PowerShell logs all entries in the Windows Powershell log, including the password. I can't explain why the title change is persistent in PS and why it isn't in VB. Does anybody has an idea? Thanks for any help in advance!
Kind regards, Eric
Extra code added, to give an example of my rename issue in VB:
Imports System.Threading
Public Class Form1
Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (ByVal hwnd As Integer, ByVal lpString As String) As Integer
Public Shared proc As New Process
Public Shared h_wnd As Integer
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
proc = Process.Start("cmd.exe", "/k title My new title & powershell.exe")
Thread.Sleep(2000)
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
h_wnd = proc.MainWindowHandle
SetWindowText(h_wnd, "My new title")
End Sub
End Class
To set cmd.exe
's window title once, on startup:
You can use cmd.exe
's CLI and its internal title
command to persistently set the new console window's title:
proc = Process.Start("cmd.exe", "/k title Test Text")
/k
creates an interactive cmd.exe
session that stays open, and accepts a startup command (by contrast, /c
executes the given startup command and then exits - see cmd /?
)
title
sets the window title; do not enclose the argument in "..."
, because the "
will then be retained in the title. If your title contains cmd.exe
metacharacters (other than spaces), such as &
, escape them individually with ^
.
A title set this way stays in effect unless overwritten by a console application called from the session, while that application is running.
If a console application does not modify the title, cmd.exe
appends the invocation command line to its title, for the duration of the execution (e.g., Test Text - some.exe foo bar
), but only in an interactive session, i.e. one launched with cmd /k
(plus a startup command) or without any arguments; by contrast, this does not happen with cmd /c
.
Once the application exits, the original title is restored, in either case.
If you need to undo the effects of a title modification performed by a console application while it is running, then an asynchronous approach seems like the only option, which cannot be achieved with cmd.exe
alone, because it doesn't support background jobs.
Both setting the original window title with title
and
your asynchronous VB.NET approach of calling SetWindowText()
later actions are needed, because when you use SetWindowText()
to change the console window title later, cmd.exe
doesn't realize that that happened and reverts to its title on interacting with the window, such as when selecting text, which is what you saw.
You can make your SetWindowText()
approach more deterministic by waiting in a loop until the title actually changes from its startup value, and then make the call, analogous to what your PowerShell code does, only from outside the process.
Because as noted above, using /k
makes cmd.exe
append the invocation command line to its window title while a program launched from it is running, so that on interacting with the window, you won't just see Test Text
, but something like Test Text - some.exe foo bar
.
Therefore, call with cmd /c
, and make a nested cmd /k
call after executing your console application in order to then enter an interactive session; e.g.: (some.exe
is a sample executable name, and foo
and bar
are sample arguments - adapt as needed):
Process.Start("cmd.exe", "/c title Test Text & some.exe foo bar & cmd /k")
Alternatively, if you simply want to keep the window open until the user presses a key (i.e. if you don't need an interactive cmd.exe
session after execution ends), replace cmd / k
with pause
; or simply omit a final command if the window doesn't need to stay open after termination at all.
To put it all together (again, change the sample some.exe
call as needed):
Imports System.Threading
Public Class Form1
Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (ByVal hwnd As Integer, ByVal lpString As String) As Integer
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim proc As New Process
' Note: special chars. such as "&", ">", "|", ... would require "^"-escaping
Dim customTitle As String = "Test Text"
proc = Process.Start("cmd.exe", "/c title " & customTitle & " & some.exe foo bar & cmd /k")
' Wait until the console application has changed the window title.
Do
' Note: It is assumed that 300 msecs. are enough for the intitial `title` command to take effect.
Thread.Sleep(300)
Loop Until Not proc.MainWindowTitle.StartsWith(customTitle)
' The console application has changed the window title,
' change it back.
SetWindowText(proc.MainWindowHandle, customTitle)
End Sub
End Class
Note:
You may want to implement a timeout for the loop that waits for the window title to change, so that you don't get stuck in an endless loop if your console application fails to launch, for instance.
The assumption is that your console application only changes its window title once, on startup.
Update: You report that this is apparently not true, so you'd need a loop that keeps monitoring for title changes throughout the life time of the console application, which in turn requires creating a dedicated thread that performs this monitoring, so as not to block your GUI.
It is unclear why, but you state that with your PowerShell approach a one-time restoration of the original title is sufficient (in that case, you do properly set the console window title, due to running inside the console); therefore, it's simplest to stick with your PowerShell approach, whose password-logging problem you can avoid by providing the password via an environment variable that you must set in your VB.NET application first, as discussed in this answer to your follow-up question.