Click here to Skip to main content
15,867,594 members
Articles / Programming Languages / C++/CLI
Article

MaChat - a chat with a browser for LANs

Rate me:
Please Sign up or sign in to vote.
4.97/5 (27 votes)
30 Jul 200211 min read 581.4K   12.4K   119   50
This article shows how to create a Chat for Local Area Networks which uses the WebBrowser control to display the messages.

App screen

Introduction

Recently I've been asked by my friends to write a chat for LAN. I thought it's a great idea. Meanwhile the MC++ competition was announced so it encouraged me to put more effort into making the app. And, of course, I decided I go in for the competition. In this article I'll describe what the program does and how it works. I demonstrate how to reuse some of the implemented features.

I split the text into several parts:

MaChat

MaChat - the name was thought up by my friend - is a chat for Local Area Networks. It supports HTML because it's based on the IE control. The chat converts emoticons to the images which come from CP thanks to Chris Maunder. Moreover, the user can browse the net inside the IDE.

MaChat was tested in a LAN. 3 systems take part in the tests: 2 WinXP and Win98 SE. I'm still testing it so if I find any bug or get a bug report I'll correct it and update the article.

Below you can see 3 screens:

MaChat with CodeProject orange schema

Image 3

Image 4

Installation process

I provided two setups. The smaller one - 356 Kb - is intended for systems were the .NET SDK is installed. The bigger one is - 1,25 Mb - for systems were only the .Net redistributable are installed. It's caused by the fact that when the .NET SDK is installed the mshtml.dll file ( an interop assembly of the mshtml.dll) is present in the system. Otherwise it must be added to the setup.

Network

The heart of every chat is network communication because the application's performance depends on it. First of all, I had to choose the best protocol and communication type. UDP\IP is connectionless and doesn't make unnecessary network traffic so it's suitable. Next, I had to select between broadcasting and multicasting. I think that multicasting is better because it only sends data to the computers which are in the group. This type of connection reduces the network traffic. In .NET Framework there is a class which wraps the UDP protocol and multicasting - System::Net::UdpClient.

I've written a library in MC++ called ChatLibrary which is responsible for sending and receiving data. It's channel-divided and message-orientated which means that if you want to do something you need to send a suitable message to a suitable channel.

ChatLibrary::MessageHeader

Below you can see the MessageHeader class definition and the MessageType enumeration. MessageHeader holds data which will be send. First field - Type - describes the aim of the message. For example, you want to refresh the remote users list and their information, all you need to do is to send a message of type MessageType::Refresh and the remote hosts will answer with a message of type UserInfo containing an UserInfo class in the Object field. In Object field you can store whatever you want but the object must be derived from System::Object. Furthermore, it and each of it's members must be marked with a [Serializable] attribute because the instance is serialized into a stream using Binary Formatter, send and deserialized on the other sides. Third field Sender contains the sender's EndPoint (IP address and port ). It's always set by the Send function of either Chat or Channel classes. The same is with the last field - Channel, which specifies the destination channel.

You can manually create a MessageHeader instance and send it or you can you high-level functions which will do the job for you.

MC++
[Serializable]
public __gc class MessageHeader
{
public:
    // Constructors
    MessageHeader::MessageHeader();
    MessageHeader::MessageHeader( MessageType eType );

public:    
    __property MessageType get_Type ( ) { return m_eType; }
    __property void set_Type ( MessageType eType ) { m_eType = eType ; }
            
    __property Object* get_Object ( ) { return m_obObject; }
    __property void set_Object ( Object* obObject ) { m_obObject = obObject; }
                
    __property IPEndPoint* get_Sender ( ) { return m_ipeSender; }
    __property void set_Sender ( IPEndPoint* ipeSender ) { m_ipeSender = ipeSender; }
            
    __property String* get_Channel ( ) { return m_strChannel; }
    __property void set_Channel ( String* strChannel ) { m_strChannel = strChannel; }

private:
    String* m_strChannel;        // Destination channel
    MessageType m_eType;        // Message type
    Object* m_obObject;            // Object
    IPEndPoint* m_ipeSender;    // Sender's IPEndPoint
};

// Message types
[Serializable]
public __value enum MessageType
{
    Unknown             = 0,    // Unknown message

    // User
    Join                = 1,    // Join the chat or a channel
    Leave               = 2,    // Leave the chat or a channel
    Refresh             = 3,    // Refresh users list
    UserInfo            = 4,    // Contain user info

    Invite              = 5,    // Invite user to join the channel
        
    // Topic
    Topic               = 6,    // Contain a new topic
    CheckTopic          = 7,    // Topic request
    CheckTopicResponse  = 8,    // CheckTopic response
        
    // Visual messages
    Text                = 9,    // Simple text
    Image               = 10,    // Image message
    Note                = 11,    // Contain a short message/note

    // Other
    Beep                = 12,    // Beep message
};

ChatLibrary::Chat

Chat is the main class which connects to a specified multicast group and opens a port for listening. After creating the Chat class you should initialize the LocalUser field which represents a local user. You can specify user's nickname, image, state, focus state and tag. Next, set EventHandlers to receive the notification of the common events. Chat class has the following ones:

MC++
// Events
__event MessageEventHandler* UserJoinMsg;
__event MessageEventHandler* UserLeaveMsg;
__event MessageEventHandler* UserUpdateMsg;
__event MessageEventHandler* InviteMsg;
__event MessageEventHandler* NoteMsg;

__event MessageEventHandler* UnknownMsg;
__event MessageEventHandler* NotHandledMsg;

After that you need to call Chat->Join function with local port number, multicast group IP and multicast group port. Currently, the local port number and multicast group port must be the same. The multicast group IPs are addresses between 224.0.0.0 and 239.255.255.255 but several of them are reserved: 224.0.0.0, 224.0.0.1, 224.0.0.2, 224.0.1.1, 224.0.0.9, 224.0.1.24.

MC++
m_chChat = new Chat();

// Set LocalUser info
m_chChat->LocalUser->NickName = "Michael";
m_chChat->LocalUser->State = UserState::Normal;
m_chChat->LocalUser->Image = image;                // bitmap

// Set EventHandlers
m_chChat->UserJoinMsg += new MessageEventHandler( this, OnUserJoin );
m_chChat->UserLeaveMsg += new MessageEventHandler( this, OnUserLeave );
m_chChat->UserUpdateMsg += new MessageEventHandler( this, OnUserUpdate );

// Join the net
m_chChat->Join( 7001, new IPEndPoint( IPAddress::Parse( "239.255.255.255" ), 7001 );

After a while the remote users will send messages of type MessageType::UserInfo and a delegate Chat->UserJoinMsg will be invoked giving you a chance to update the users list. Property UsersInfo of type UserInfoCollection holds the actual remote users list during the whole session of the chat.

ChatLibrary::Channel

As I said earlier ChatLibrary is channel-divided. This feature allows you to create public, private channels (currently, it isn't supported by the library but you can achieve this, see MaChat\ChatProxy.cpp: CreateChannel and CreateChannelP functions) and channels with a specific target. The list of all channels is available through Channels property

Creating a channel is a very easy task. Look at the following example:

MC++
Channel* channel = new Channel( m_chChat );
channel->Name = strName;
channel->ConnectionType = ChannelConnectionType::MulticastGroup;

// Set EventHandlers
channel->UserJoinMsg += new MessageEventHandler( this, OnUserJoin );
channel->UserLeaveMsg += new MessageEventHandler( this, OnUserLeave );
channel->TextMsg += new MessageEventHandler( this, OnText );
channel->ImageMsg += new MessageEventHandler( this, OnImage );
channel->TopicChanged += new ChangeEventHandler( this, OnTopicChange );

channel->Join();
channel->CheckTopic();

First of all, you need to create a Channel class. The constructor takes one parameter: pointer to the ChatLibrary::Chat class. Then set the name of the newly created channel and it's connection type. And here we stop for a while because the connection type requires some explanation. For the channels with a small number (2-3) of users you can use direct connection so set ConnectionType to Direct, for large ones use MulticastGroup and the data will be send to all users through the multicast group. Use Auto for dynamically channels

MC++
public __value enum ChannelConnectionType
{
    Auto            = 0,    // Switch between MulticastGroup and Direct
                            // depends on the number of the users
    MulticastGroup  = 1,    // Use multicast group to communicate
    Direct          = 2,    // Send data directly to the remote hosts
};

Next, you can set EventHandlers. Some of them are similar to the Chat delegates like UserJoinMsg but some don't occur in the Chat class, for example, TextMsg, TopicChanged. Finally, call Join function ( if the channel with the same name already exist the exception will be thrown ). Furthermore, if you joining an existing channel it's good to get its topic so call CheckTopic.

Now you're ready to send messages. You can do it by calling Send function which is overloaded both in Chat and Channel classes.

WebBrowser

I decided to use AxWebBrowser control ( IE control ) as a channel browser because it gives a great flexibility and simplicity to format text and images. If the user knows HTML then he/she can change text size, color, decoration, or event send a table. Detailed information about inserting the control you can get from Nikhil Dabas's article - Using the WebBrowser control in .NET. Here I'll describe how to parse and edit HTML documents during runtime, how to invoke scripts and how to handle context menu

But before we start, we need to create an interop assembly from mshtml.dll. The file contains about 4000 interfaces which supports parsing the HTML document. It's not required if the .NET Framework SDK is installed but you must redistribute the file with your app or create it during the install process. All you need to do is to run a tlbimp utility:

MC++
tlbimp c:\windows\system32\mshtml.dll

and wait a few minutes because the library is big - the output file is about 8 MB. Next, copy the file to the app directory.

Channel browser

Now it's time to start the show. I'll try to explain you how the channel browser works. Below you can see it.

Cool HTML formatting

The page consists of 2 frames, the upper has id header and the lower one has id main. After the page is loaded the app parses the document and try to gets interfaces required to operate the page. These are:

MC++
mshtml::IHTMLWindow2* m_windowMain;
mshtml::IHTMLWindow2* m_windowHeader;
mshtml::IHTMLElement* m_elementMainBody;
mshtml::IHTMLElementCollection* m_arLinks;

The first 2 are the frame representation and allow to invoke scripts. The interface mshtml::IHTMLElement is a base for every HTML tag. We need to get the body element of the lower frame. And finally the interface mshtml::IHTMLElementCollection which is a collection of all links in the document.

First of all, we need to get the document interface - mshtml::IHTMLDocument2. It is stored in the AxWebBrowser->Document. Next, obtain the whole document window doc->frames and it's frames collection.

MC++
IHTMLDocument2* doc = static_cast<IHTMLDocument2*>( this->Document );

if ( doc )
{
    // Obtain window interface
    IHTMLWindow2* windowFrame = dynamic_cast<IHTMLWindow2*>( doc->frames );
                    
    if ( windowFrame )
    {
        // Check if the window is framed
        IHTMLFramesCollection2* framescol;
        framescol = windowFrame->frames;

If the collection is a valid pointer try to get the frames from the ids. Having obtaining the main frame pointer get the body element doc->body and links collection doc->links.

MC++
        if ( framescol )
        {
            // Try to get frame with ID main
            String* strFrame = "main";
            Object* objName = static_cast<Object*>( strFrame );
            Object* obj;
            obj = framescol->item( &objName );
            if ( obj )
            {
                // Covert to IHTMLWindow2 interface and get document
                m_windowMain = static_cast<IHTMLWindow2*>( obj );
                            
                IHTMLDocument2* doc = m_windowMain->document;
                if ( doc )
                    m_elementMainBody = doc->body;

                // Furthermore obtain link collection interface
                m_arLinks = static_cast<IHTMLElementCollection*>( doc->links );
            }

            // And then with ID header
            strFrame = "header";
            objName = static_cast<Object*>( strFrame );
                        
            obj = framescol->item( &objName );
            if ( obj )
            {
                // Covert to IHTMLWindow2 interface
                m_windowHeader = static_cast<IHTMLWindow2*>( obj );
            }
        }
    }
}

Next, we're ready to update the document with the discussion text. It's done by the code shown below.

MC++
if ( m_windowMain && m_elementMainBody )
{
    // Add
    String* strBody = m_elementMainBody->innerHTML;
    strBody = String::Format ( "{0}{1}", strBody, strHTML );
    m_elementMainBody->innerHTML = strBody;
}

One thing is not working correctly so far - links. We need to hack them that they open in a new window. ( currently, an external browser opens ). So go through the links collection and set every target property to _BLANK

MC++
// Parse links
for ( int i=0; i<m_arLinks->length; i++  )
{
    Object* obj = static_cast<Object*>( __box(i) );
    IHTMLAnchorElement* anchor = static_cast<IHTMLAnchorElement*>
                                                ( m_arLinks->item( obj, obj ) );
    if ( anchor )                        
        anchor->target = "_BLANK";
}

Scripts

If we got an interface pointer to the window - mshtml::IHTMLWindow2 - we can invoke scripts which the page contains. The code below sets the topic and the channel's name in the header. It's done by calling a javascript which is located in the header frame and it's called UpdateHeader. Before we do that we need to double \ and ' characters. Otherwise the control will throw an exception.

MC++
// We must double \ and ' characters. Otherwise an exception will be thrown.
strChannel = strChannel->Replace( "\\", "\\\\" );
strChannel = strChannel->Replace( "\'", "\\\'" );
strTopic = strTopic->Replace( "\\", "\\\\" );
strTopic = strTopic->Replace( "\'", "\\\'" );

// Create JavaScript function call
String* strHTML = String::Format( "UpdateHeader( \'{0}\', \'{1}\' );",
    strChannel, strTopic );

try
{
    // Invoke
    m_windowHeader->execScript( strHTML, new String("javascript") );
}
catch ( Exception* e )
{
    // Script error handling, try to reload
    LoadNewPage();    
}

Handling the context menu

All we need to do this is to define the IDocHostUIHandler interface. It's done in the WebBrowserEx.h file. Next, we need to implement the interface and set it. The IDocHostUIHandler can be implemented as a simple class which derives the interface. In the chat the class MaChat::WebBrowser::WebBrowserEx does the whole job.

MC++
// Set the UI handler for the browser to this application
IHTMLDocument2* doc = dynamic_cast<IHTMLDocument2*>( this->Document );
ICustomDoc* custom = dynamic_cast<ICustomDoc*>( doc );
custom->SetUIHandler( static_cast<IDocHostUIHandler*>( this ) );

Now the function ShowContextMenu will be called every time the context menu will be required.

MC++
void WebBrowserEx::ShowContextMenu( unsigned int dwID, tagPOINT* ppt, 
    [MarshalAs(UnmanagedType::IUnknown)] Object* pcmdtReserved, 
    [MarshalAs(UnmanagedType::IDispatch)]Object* pdispReserved)

You can cast the first parameter to type WebBrowser::ContextMenuConstants and you will know what type of menu the control wants. The second is a click location. The third parameter specifies document's interface and the last one is the interface to the element at the screen coordinates specified in ppt.

User Interface

MagicLibrary

The user interface makes use of MagicLibrary. It's a freeware third-party product written in C#. It supports several cool features which makes program looks nicer. It provide a simple way to change color so the interface appearance can be altered. MaChat provides only 5 build-in schemas so far. Another worth mentioning feature, is docking windows. I placed the users list and tollbars in this windows. The user can dock or hide them whatever he/she wants.

Globalization

MaChat uses the new .NET feature - globalization. The user can change the language of the user interface. (Currently, English and Polish). VS.NET almost doesn't support this feature so I had to do it by myself. Here I'll briefly describe you how you can do it for a single class.

A good idea is to create a new empty project called, for example, Resources. It helps in management. After that, go into the project's options and set Configuration Type to Utility.

Image 6

After that, you need to create resX files. One for each language. The picture above shows a resX file opened in the VS.NET. The first column - name - contains the string's IDs, the second one contains text which will be returned. You create as many files as many languages you want to have.

Now the compilation. Go to the project properties window - Building events node. There's a property called Command line. If you click on it a new window appears. Here you need to enter the following line:

resgen MaChat.MainForm.resX $(OutDir)\MaChat.MainForm.resources

for each file you want to include in your project. Note that the resource file must have appropriate name. If you want to use it in a class, for instance, MyProgram::MyForm you need write

resgen Whatever.resX $(OutDir)\MyProgram.MyProgram.resources

If the file contains neutral language (the one compiled in the exe) you don't need to specify any culture identifier ( for available identifiers see .NET SDK - CultureInfo class ). Otherwise you must add it at the and of the name but before the resource extension. For a Polish resource you need to write

resgen Whatever.resX $(OutDir)\MyProgram.MyProgram.pl.resources

As I said the neutral resources are build in the exe. The rest are compiled into the satellite assemblies, one per language. To the command line you must add the following text:

al /out:$(OutDir)\pl\MyAppName.resources.dll /c:pl
/embed:$(IntDir)\MaChat.MainForm.pl.resources,MaChat.MainForm.pl.resources,Private

replacing the MyAppName with yours, pl ( /c:pl ) with the language identifier for this assembly and the name of the resources files in the embed switch. The embed switch syntax is as follow:

/embed:[path of the resource file],[the namespace through which you will access + 
       language identifier + resource extension],[Access specifier]

Each assembly must be located in [app dir]\[culture identifier] directory.

Next we need to add the neutral resources to the exe. Go to the properties window of the app project - Linker\Input node and add all neutral resource files' path to the Embed Managed Resource File property.

Now you're ready to use the ResourceManager in your code. It's very simple:

MC++
m_resourcesText = new System::Resources::ResourceManager( 
                                           __typeof( MyProject::MyForm ) );
String* strText = m_resourcesText->GetString( strID );

The return string will be of language specified in the

System::Threading::Thread::CurrentThread->CurrentUICulture->Name
property. If it's not available the neutral will be returned. You can change the current language using the code shown below:

MC++
cultureNeutral = new System::Globalization::CultureInfo( "" );
culturePolish = new System::Globalization::CultureInfo( "pl-PL" );

System::Threading::Thread::CurrentThread->CurrentUICulture = cultureNeutral;

Acknowledgements

Morover I want to thanks the guys listed below:

  • The authors of the MagicLibrary
  • Maciej Piróg - for help and his icons
  • Chris Maunder - for CP emoticons

Contact

If you have any questions, bugs reports or opinions you can send them to GreenSequoia@wp.pl

History

31 July 2002 - updated downloads

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect macmichal.pl
Poland Poland
Micheal is an independent consultant - www.macmichal.pl.
He's main areas of interest are: DDD\CqRS, TDD, SaaS, Design Patterns, Architecture. He specializes in .Net/C# for the early beginning of it and T-SQL. He's a writer, blogger (blog.macmichal.pl) and speaker.

In his spare time, he's climbing the mountains all over the Europe.

Comments and Discussions

 
GeneralI build the src in vs2005,there has some errors. Pin
poptang27-Apr-10 22:22
poptang27-Apr-10 22:22 
Generalemotions Pin
coolremo10-Jun-08 2:20
coolremo10-Jun-08 2:20 
Generalresource files in managed C++ Pin
varundj8-Mar-07 23:52
varundj8-Mar-07 23:52 
GeneralUnAuthorizedAccessException accessing frame document Pin
Goran Kostadinov3-Mar-06 6:08
Goran Kostadinov3-Mar-06 6:08 
GeneralRe: UnAuthorizedAccessException accessing frame document Pin
sorinceldesteptrau18-Jun-06 20:53
sorinceldesteptrau18-Jun-06 20:53 
Generalseems nice chat, but.. Pin
rex_plantado9-Dec-04 21:08
rex_plantado9-Dec-04 21:08 
GeneralRe: seems nice chat, but.. Pin
Michael Mac21-Dec-04 4:40
Michael Mac21-Dec-04 4:40 
QuestionRe: seems nice chat, but.. Pin
gs_virdi1-Sep-08 23:02
gs_virdi1-Sep-08 23:02 
Generalcompile in .Net2003 Pin
zbyyxl21-Mar-04 21:49
zbyyxl21-Mar-04 21:49 
GeneralMa-Chat Pin
M. Aslam23-Dec-03 13:38
M. Aslam23-Dec-03 13:38 
GeneralMessages in window form Pin
jelleo11-Nov-03 6:28
jelleo11-Nov-03 6:28 
GeneralThreadStateException Pin
berserkblue1-Oct-03 21:59
berserkblue1-Oct-03 21:59 
General.Net 2003 Compilable Source Pin
Aussiephoenix29-Sep-03 22:52
Aussiephoenix29-Sep-03 22:52 
GeneralRe: .Net 2003 Compilable Source Pin
Aussiephoenix30-Sep-03 0:29
Aussiephoenix30-Sep-03 0:29 
GeneralAn exception of type System.ArgumentException has been thrown Pin
FooOfTheBar4-Aug-03 1:04
FooOfTheBar4-Aug-03 1:04 
QuestionA question or a bug? Pin
winart17-Jul-03 22:31
winart17-Jul-03 22:31 
AnswerRe: A question or a bug? Pin
Michael Mac20-Jul-03 9:05
Michael Mac20-Jul-03 9:05 
GeneralPowerFull but I have got a problem Pin
paul.hogie30-Jun-03 23:51
paul.hogie30-Jun-03 23:51 
GeneralPowerFull but I got a problem Pin
paul.hogie30-Jun-03 23:26
paul.hogie30-Jun-03 23:26 
GeneralWonderful Pin
yingyuheng23-Apr-03 4:59
yingyuheng23-Apr-03 4:59 
GeneralRe: Wonderful Pin
Michael Mac25-Apr-03 9:19
Michael Mac25-Apr-03 9:19 
GeneralSetUIHandler depends on IE6 ?! Pin
ActsOn16-Apr-03 4:11
ActsOn16-Apr-03 4:11 
GeneralWhat's the meaning of "CP emoticons" Pin
psusong27-Mar-03 16:34
psusong27-Mar-03 16:34 
GeneralRe: What's the meaning of "CP emoticons" Pin
Taka Muraoka27-Mar-03 17:36
Taka Muraoka27-Mar-03 17:36 
Emoticons are the little smiley pictures you can insert into your posts. You can see them across the bottom of the space where you type in your message.

Chris Maunder is just some dude who hangs out here every now and then Smile | :)



Software is everything. It also sucks. Charles Fishman [^]

Awasu 1.0.2 (beta)[^]: A free RSS reader with support for Code Project.

GeneralRe: What's the meaning of "CP emoticons" Pin
Nitron27-Mar-03 17:42
Nitron27-Mar-03 17:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.