Click here to Skip to main content
Email Password   helpLost your password?

Overview and Background

Microsoft's terminal services client (also called 'Remote Desktop Connection') has one main thing against it. Remote applications do not appear as if they are running on the local desktop, instead they appear in a separate window which represents the server's desktop. This is fine if you just want to work exclusively on the server, but can be a pain if you want to switch between applications on the server and the local desktop or want to run applications on different servers. What is needed is a way to display the remoted applications as 'Seamless Windows' on the client.

Commercial products have been written to achieve this in a Windows environment, the most well known would be Citrix. Citrix uses its own protocol (ICA) to publish applications to the client. Others have used Microsoft's protocol called RDP (Remote Desktop Protocol) with additional software to achieve the same effect (the most notable of these is Tarentalla's Canaveral IQ � I suspect they use a similar, but more sophisticated, method to the one presented in this article).

While these products provide a lot more than just seamless windows, they are also quite expensive. It would be nice to have this feature in a regular RDP client without having to buy a whole application publishing product.

This article provides a possible solution to this problem by extending Microsoft's RDP client using virtual channels to communicate between the server and the client. This option has been chosen over writing or extending an existing open source RDP client (such as rdesktop) because we will still be able to take advantage of all the features in Microsoft's client (and presumably all new features they add in the future). Also, an advantage to using Microsoft's client is that we can get some rudimentary application publishing over a web page since their terminal services client has an ActiveX component to do this.

Concept And Approach

Introduction

The RDP protocol does not provide any information about the open windows, all it does is send a bitmap of the server screen down to the client and route mouse and key presses back to the server. To make it appear that applications are running on the local machine, we need to 'clip' away the server desktop on the client.

To do this, it is quite obvious that we are going to need to know what windows are on the server and where they are. For this, we can use global hooks to find out what is happening in the server session (windows opening, closing, minimizing etc.). Next, we need to communicate this information back to the client, this is where virtual channels come in. They are Microsoft's mechanism for two way communication between a server and client when a terminal services session is open. They are mainly used for transferring files and printing but we can use it to send window information back to the client. Once the client has this information, it can then use it to 'clip' the server desktop to just show the applications running on the server.

Hooking into Window Messages On The Server

At this point, I must give credit to Markus Rollmann. I have used his Code Project article as a basis to write this part of the software. This part of the application runs on the server and opens the server side of the virtual channel. To use global hooks, there is a separate DLL (called 'hookdll.dll'). This DLL monitors what is happening to the windows on the server and sends the appropriate string messages down the virtual channel which can then be interpreted by the client code.

An interesting thing I learnt here is that you can't get WH_SHELL notifications from global hooks if there is no registered shell. Normally, 'explorer.exe' is running as a shell, but in a terminal services session, you usually run just the application without the shell. To get round this (and it took me a long time to figure out why I wasn't getting these notifications), you need to register your application as the shell to replace 'explorer.exe' (see the code on how to do this).

Our application on the server ('clipper.exe') is now also our shell. When we launch our session from the client, we set the starting application as 'clipper.exe'. This ensures all the global hooks are setup before any applications are launched on the server. One of the parameters to 'clipper.exe' is the path of the application to start, which 'clipper.exe' launches as a new process. In addition, it will monitor this process so that when the process has exited, 'clipper.exe' can also close. This will then result in the session logging off as shell application has closed.

Responding to the messages on the client

Virtual channels work by having an executable on the server sending information back to the client. On the client side, there is a DLL that is loaded by the terminal services client when it loads which can listen for the information coming down the virtual channels ('TswindowClipper.dll' � this is loaded by setting a key ("Name" = "TSWindowClipper.dll") in the registry at: HKEY_CURRENT_USER\Software\Microsoft\Terminal Server Client\Default\AddIns\TSWindowClipper). Our client DLL code maintains a hashtable which contains the location of all the windows on the server and their state. The client DLL code is then able to calculate a region that matches the windows on the server and 'clip' the terminal services client window appropriately (the client DLL gets a handle to the terminal services client window when it starts). When windows are moved, resized, opened, closed, minimized or maximized on the server, a message gets sent to the client DLL which is then able to recalculate this region and clip the window accordingly. Another thing the client does is to create dummy taskbar items that represent the windows open on the server.

Publishing Applications On the Web

It is possible to launch a seamless terminal services session from a web page using the remote desktop ActiveX control. To do this, we need to tell it to use our client side Virtual channel DLL. This is how we do it:

MsRdpClient.SecuredSettings.StartProgram = "c:\tswinclipper\clipper.exe notepad.exe"
MsRdpClient.AdvancedSettings.PluginDlls = "tswindowclipper.dll"
MsRdpClient.FullScreen = TRUE
MsRdpClient.Width = screen.width
MsRdpClient.Height = screen.height

We need to set the 'StartProgram' to the correct path for 'clipper.exe' and the application we want to launch on the server. Here is an example HTML file (your will need to get the 'msrdp.cab' file for the ActiveX control from here and put it in the same directory as this HTML page):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>

<head>
<title>Remote Desktop Web Connection</title>
<meta content="JavaScript" name="vs_defaultClientScript">
</head>

<body>

<script language="vbscript">

sub BtnConnect

MsRdpClient.server = "<%YOUR SERVER%>"
MsRdpClient.UserName = "<%YOUR USERNAME%>"
MsRdpClient.AdvancedSettings.ClearTextPassword="<%YOUR PASSWORD%>"
MsRdpClient.Domain = "<%YOUR DOMAIN%>"

MsRdpClient.SecuredSettings.StartProgram = "c:\tswinclipper\clipper.exe notepad.exe"

MsRdpClient.AdvancedSettings.PluginDlls = "tswindowclipper.dll"

MsRdpClient.FullScreen = TRUE
MsRdpClient.Width = screen.width
MsRdpClient.Height = screen.height

'These 2 do not work from the activeX control

'MsRdpClient.AdvancedSettings2.DisplayConnectionBar = FALSE
'MsRdpClient.AdvancedSettings2.PinConnectionBar = TRUE

'Device redirection options

MsRdpClient.AdvancedSettings2.RedirectDrives = FALSE
MsRdpClient.AdvancedSettings2.RedirectPrinters = TRUE
MsRdpClient.AdvancedSettings2.RedirectPorts = FALSE
MsRdpClient.AdvancedSettings2.RedirectSmartCards = FALSE

'Connect

MsRdpClient.Connect

end sub

sub MsRdpClient_OnDisconnected(disconnectCode)

'goback to the page that called this one

history.go(-1)

end sub

</script>

<br>
<object language="vbscript" id="MsRdpClient" onreadystatechange="BtnConnect" 
        codebase="msrdp.cab#version=5,1,2600,1050" 
        classid="CLSID:9059f30f-4eb1-4bd2-9fdc-36f43a218f4a">
</object>
<br>

</body>
</html>

How To Install And Run

Here are the main things to do:

  1. Run 'setup.exe' on the server and client. Then run the terminal services client or remote desktop connection on the client.
  2. Resolution must be set to full screen and the connection bar must not be displayed.
  3. The start program shell must be the full path to 'clipper.exe' followed by one parameter which is the application on the server you want to launch seamlessly.
  4. 'Show contents of window while dragging' must be turned on in the connection setting, otherwise moving or sizing windows will not work.
  5. It is also important to note that the 'Show contents of window while dragging' also has to be enabled on the server as well.
  6. It is also a good idea to turn the desktop background off to save bandwidth, as we will be clipping the desktop anyway.

There is a PDF called 'windowclipper.pdf' that is installed with the client setup. Have a look at that if you have any problems.

Limitations

Acknowledgments

Revision History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralWorks great but it has one flaw
JustinKubicek
19:53 13 Jun '09  
It doesn't work with "Start in the following folder." The application in treated as if clipper was not even involved.
GeneralIT farm
pyromate212
12:54 17 May '09  
http://www.itfarm.co.uk/[^]

does anyone know what software they use? it's RDP + a custom activeX ? look quite similar to the stuff were doing here
GeneralProblem running Terminal Services Window Clipper in a seamless window
vipersixtyseven
5:08 17 Mar '09  
hi
first sorry for my bad english.
the terminal service window clipper sounds like a nice application but i have a problem running Terminal Services Window Clipper in a seamless window.
on server and client running winxp pro sp2.
i try the example.rdp and i see the complete desktop not the seamless window.
then when i start the clipper.exe on the server manually and then start any application the seamless windows on the client is running.
the only changes on the example.rdp is username, and domainname.
can anyone help me ?
thanx
vipersixtyseven
GeneralRe: Problem running Terminal Services Window Clipper in a seamless window
uppi53
8:30 17 Mar '09  
Hi, you will see the full desktop when logging on as a user who already is logged on on the 'server', means you first should logoff there or create another account and logon as the other user when testing the clipper.

uppi53

GeneralNewer code
Member 2145303
9:13 31 Dec '08  
Has anyone tried to mesh in the changes from the rdesktop project and see if it works as well?

http://rdesktop.cvs.sourceforge.net/viewvc/rdesktop/seamlessrdp/[^]
GeneralRe: Newer code
uppi53
8:45 17 Mar '09  
unfortunately the versions 1.5 and up need not only cygwin.dll but cygwin installed and started an x-session.
Seamless windows are using the -A switch which is not in older versions.

uppi53

GeneralRe: Newer code
Member 2288736
18:40 20 Apr '09  
Hmmmm.......the seamlessrdp source includes code for a slightly modified version of the tswindowclipper client dll that can be used with seamlessrdpshell.exe as the explorer.exe shell replacement on the server (replacing clipper.exe, since seamlessrdpshell.exe seems to be more fully developed).

Both the old and new versions of the clipper client dll source compile using Visual C++ 2005 Express Edition (the "free as in free beer" version).

And tinkering with the clipper.cpp client dll code to add some of the functions found in rdesktop's seamless.c looks like a promising path to making this really work.

Now what about the system menu handling for the task bar icons representing each ws_popup window created on the client, and sending the "minimize", "restore", "close", and "maximize" message back to the server via the seamless RDP vchannel?

snippets of (poorly) changed/added code for clipper.cpp follow:

void WINAPI VirtualChannelOpenEvent( DWORD openHandle, UINT event,
LPVOID pdata, UINT32 dataLength,
UINT32 totalLength, UINT32 dataFlags )
{
LPDWORD pdwControlCode = ( LPDWORD ) pdata;
CHAR ourData[ 1600 ];
UINT ui = 0;

UNREFERENCED_PARAMETER( openHandle );
UNREFERENCED_PARAMETER( dataFlags );

ZeroMemory( ourData, sizeof( ourData ) );

//copy the send string (with the same lenth of the data)
strncpy( ourData, ( LPSTR ) pdata, dataLength / sizeof( char ) );

if ( OUTPUT_DEBUG_INFO == 1 ) {
OutputDebugString
( " TS WINDOW CLIPPER :: CLIENT DLL :: Info --> Virtual channel data received " );
OutputDebugString( ourData );
}

if ( dataLength == totalLength ) {
switch ( event ) {
case CHANNEL_EVENT_DATA_RECEIVED: {
char *p;
char *tok1, *tok2, *tok3, *tok4, *tok5, *tok6, *tok7, *tok8;
//unsigned long id, flags, group, parent;
char *endptr;
p = ourData;

tok1 = Get_Token(&p);
tok2 = Get_Token(&p);
tok3 = Get_Token(&p);
tok4 = Get_Token(&p);
tok5 = Get_Token(&p);
tok6 = Get_Token(&p);
tok7 = Get_Token(&p);
tok8 = Get_Token(&p);

CWindowData* wid=new CWindowData("");

if ( strcmp(tok1, "CREATE" ) == 0 ) {
if ( OUTPUT_DEBUG_INFO == 1 ) {
OutputDebugString
( "TS WINDOW CLIPPER :: CLIENT DLL :: Info --> Message was of type CREATE window title is:" );
OutputDebugString( wid->GetTitle() );
}
if (tok6 != NULL ) {
wid->SetId(tok3);
CStdString s = wid->GetId();
char *ptr;
int length = s.GetLength();
ptr = s.GetBufferSetLength( length );
hash_insert( ptr, wid, &m_ht );
CreateAndShowWindow( wid );
DoClipping( 1 );
}
} else if ( strcmp(tok1, "DESTROY" ) == 0 ) {
if ( OUTPUT_DEBUG_INFO == 1 ) {
OutputDebugString
( "TS WINDOW CLIPPER :: CLIENT DLL :: Info --> Message was of type DESTROY window title is:" );
OutputDebugString( wid->GetTitle() );
}
wid->SetId(tok3);
CStdString s = wid->GetId();
char *ptr;
int length = s.GetLength();
ptr = s.GetBufferSetLength( length );

CWindowData *oldWinData =
( CWindowData * ) hash_del( ptr, &m_ht );

DestroyTaskbarWindow( oldWinData );

delete oldWinData;
delete wid;
DoClipping( 1 );
} else if ( strcmp(tok1, "POSITION" ) == 0 ) {
if ( OUTPUT_DEBUG_INFO == 1 ) {
OutputDebugString
( "TS WINDOW CLIPPER :: CLIENT DLL :: Info --> Message was of type POSITION window title is:" );
OutputDebugString( wid->GetTitle() );
}
wid->SetId(tok3);

// check bounds, coords can be negative if window top left point is moved off the screen.
// we don't care about that since the window can't be see so just use zero.
if (strchr(tok4, '-')==NULL) {
wid->SetX1(atoi(tok4));
} else {
wid->SetX1(0);
}
if (strchr(tok5, '-')==NULL) {
wid->SetY1(atoi(tok5));
} else {
wid->SetY1(0);
}
if (strchr(tok6, '-')==NULL) {
wid->SetX2(atoi(tok4)+atoi(tok6));
} else {
wid->SetX2(0);
}
if (strchr(tok7, '-')==NULL) {
wid->SetY2(atoi(tok5)+atoi(tok7));
} else {
wid->SetY2(0);
}
CStdString s = wid->GetId();
char *ptr;
int length = s.GetLength();
ptr = s.GetBufferSetLength( length );

CWindowData *movedWinData =
( CWindowData * ) hash_lookup( ptr, &m_ht );

if ( movedWinData != NULL ) {
movedWinData->SetX1( wid->GetX1() );
movedWinData->SetX2( wid->GetX2() );
movedWinData->SetY1( wid->GetY1() );
movedWinData->SetY2( wid->GetY2() );

DoClipping( 1 );
}

delete wid;
} else if ( strcmp(tok1, "TITLE" ) == 0 ) {
if ( OUTPUT_DEBUG_INFO == 1 ) {
OutputDebugString
( "TS WINDOW CLIPPER :: CLIENT DLL :: Info --> Message was of type TITLE window title is:" );
OutputDebugString( wid->GetTitle() );
}
if (tok5 != NULL ) {

wid->SetId(tok3);
CStdString s = wid->GetId();
char *ptr;
int length = s.GetLength();
ptr = s.GetBufferSetLength( length );

CWindowData *retitledWinData =
( CWindowData * ) hash_lookup( ptr, &m_ht );
wid->SetTitle(tok4);
SetWindowTitle(retitledWinData, tok4);
}

delete wid;
} else if (strcmp(tok1,"HELLO") == 0) {
//DoClipping( 1 );
}
}
break;

case CHANNEL_EVENT_WRITE_COMPLETE: {}
break;

case CHANNEL_EVENT_WRITE_CANCELLED: {}
break;

default: {}
break;
}
} else {}
}

void SetWindowTitle(CWindowData * wd, const char *title)
{
if ( wd->TaskbarWindowHandle != NULL ) {
SetWindowText( wd->TaskbarWindowHandle , title);
}
}

char* Get_Token(char **s)
{
char *comma, *head;
head = *s;

if (!head)
return NULL;

comma = strchr(head, ',');
if (comma)
{
*comma = '\0';
*s = comma + 1;
}
else {
*s = NULL;
}

return head;
}

GeneralClient Limitation
Member 3012384
19:18 18 Dec '08  
This is a great program. But it still run only for 2 clients as depend on Server w2k3 terminal service default. Have you experienced more than 2 terminal service client ?

Thanks in advance.

Regards,
Adri
GeneralRe: Client Limitation
uppi53
8:40 17 Mar '09  
that's normal and a MS limitation, not depending on the explorer replacing mechanism. For better understanding you may look for terminalserverpatch (replacing/patching termsrv.dll, adding some reg entries and changing group policies). This will not make a 'real' terminal server but administrative remote connections without limit of users.

uppi53

Generalhow to build and link using vs6 vc++ ?
K.O.
1:32 12 Aug '08  
hi sorry for the previuos respond.
I will ask quitely now :
can you please create a vc++ 6.0 demo
for this article. I wish to learn how
to deploy these API\DLLs in older IDE.

Thanks.

only simple code

Generalhow to use it with visual studio c++ WITHOUT .NET????
ori kovacsi
23:25 23 Dec '07  
hi I wish to use the remote desktop luncher , with the password set programatically, and no password user window appear. in a simple win32 project???

I have visual studio 6.0 c++ with sdk, but cant use the .NET wizards or any sln project file.

can anybody help me pleas I am completly forstrated, "It is harder to get this knowledge the penetrating the Entrance Controll computer in the white Howse".

H H EEE L PP !!!
H H E L P P !!!
HHH EEE L PP !
H H E L P
H H EEE LLL P o

One more fan of
Simplicity.

GeneralDoes this work with RDP 6.0?
reinux
10:51 20 Jun '07  
Does this work with RDP 6.0?
GeneralRe: Does this work with RDP 6.0?
uppi53
8:46 17 Mar '09  
yes

uppi53

GeneralDoesn't work even with "notepad.exe"
kandda
16:16 3 Dec '06  
Broken output on window moving/resizing.
All the more it does not display "Visual Studio IDE" at all.

QuestionError Message... Using UNC & Switches
Joe Doherty
12:27 14 Aug '06  
This add-on would be the perfect solution, however when I put in the string for the program to start it comes up with not being able to find the program. I am going to assume it has something to do with the program string. It is a UNC & Utilizes switches (i.e. \\Servername\Share\program.exe START /y /08). I tried using quotes around the entire string and without quotes and nothing seemed to do it. Any ideas?
GeneralClipper.exe Entry point no found
cow2006
1:51 17 May '06  
I just installed the Tswindowclipper at both client and server side.
server and client are rebooted after installation.
when I try to connect the Terminal Server from the client computer, the error occurs.

Hope you guys will have solution on this.



cowcow
GeneralRe: Clipper.exe Entry point not found
cow2006
0:10 18 May '06  
I found the answer and the default RDP example connecton works fine.
But when I changed the application from notepad.exe into a custom-made retail sales application, it failed.
No error message, application didn't popup.
I need to go into Terminal Service Manager to terminate the session.

Hope you guys have solution on this.




-- modified at 22:47 Thursday 18th May, 2006
Generalclipper.exe not in setup
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
2:24 21 Apr '06  
hi,
i've downloaded the setup.exe-installer for TS Window Clipper 0.3.1. This file doesn't install (or doesn't contain?) the clipper.exe-file. Neither on Windows XP nor on Windows 2003. The file size of this installerfile is 661 KB. Can you help?

thanks
gernot
Generalclipper message
jjmccoy
2:17 9 Mar '06  
Sniff maximising windows is not allowed in this version. Sorry.

how do i get passed this error.



JOhn Mortensen
it manager
social- and helthcare education school
denmark
GeneralRe: clipper message
Martin Wickett
5:17 10 Mar '06  

I'm afraid that functionality hasn't been implemented.

Regards,
Martin.
GeneralRe: clipper message
Member 2145303
9:14 31 Dec '08  
Has that been resolved in the rdesktop version of this code?
GeneralPluggable Protocol
bcweis
10:34 15 Feb '06  
Martin, great work on this. Since you have worked with RDP I was wondering if you could answer a simple question for me:

Can the RDP session be initiated from a URL, e.g.

rdp://servername
msrdp://servername

etc, in the same manner that we can shell mailto: and ftp: sessions?

I'd like to automate a connection in this manner and can't seem to find the proper protocol name.

Regards,
Bryan
GeneralRe: Pluggable Protocol
Martin Wickett
7:45 16 Feb '06  

Hi Bryan,

I don't think you can use an url. The easiest way to automate a connection is to run the client from the command line.

If you type:

%SystemRoot%\system32\mstsc.exe /?

you will get a dialog with all the options. You can also pass in a .rdp file with a connection setup as one of the arguments.

another way you could do this is to invoke the activeX client using the windows scripting host (should be able to just reuse the vbscript code in the article).

Regards,
Martin.

QuestionAuto logoff?
Helixx
16:56 25 Nov '05  
First of all, thank for making this and second thanks a million for making it available to everyone. I do have one question... Everything works great. I have a 2003 Server running terminal services and a few test clients running XP-P SP2. My only problem is that when the users close the application, the remaining terminal session goes to full screen with no way to exit except for CTRL+ALT+DEL and killing the session. Is there some way I can kill the session on the close of the app? I am not using the web interface just the RDP client.

Thanks again,
Mike
AnswerRe: Auto logoff?
marrik
13:10 20 Jan '06  
I'm having the same problem. Any solution?

thanks.


Last Updated 15 Apr 2005 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010