![]() |
![]() |
![]() |
|||||||||||||||||
![]() | |||||||||||||||||||
TCP/IP Programming for OS/2 Inside the book
About the book
The Publisher
Order |
Chapter 10
A Simple News Client Goals for the News Client Application With the enhanced editor and Ping code behind us, we can now advance to something a little more complicated. The NNTP news reader we will create in this chapter will integrate the network interface and the PMCLASS code into a complex application featuring multiple windows and multiple network connections. Many applications being written today consist of a completely encapsulated user interface with a central application window and enclosed child windows. The news application will be a little different. The main program will consist of a control panel only. The other child windows for listing available server groups and subscriptions, as well as the message listings and articles, will all be managed as separate windows not constrained to a single parent window. Each window is created as a separate object within the code, and since the WorkPlace Shell is an object-oriented interface, we will treat each window as a distinct display element. This technique is now coming into vogue with the advent of object frameworks such as Taligent and OpenDoc. The reasons for this are obvious. Studies have shown that this is how people work. Users would like a window to have a single function, and all configuration and management of that window should reside within the window. Secondly, understanding how people work helps encapsulate and greatly simplifies the coding task. As a developer, you no longer have to worry about how the visual actions of one window affect another. Take tiling in a multiple document interface (MDI) application, for example. If the user has several MDI windows open and elects to tile them, the display of each of these windows is likely to be affected - perhaps in a way that is undesirable for the user. Using completely separate windows, we eliminate this problem and make our own programming job a lot easier. Figure 10-1 illustrates the WPS desktop during a typical session with the news reader we will build in this chapter. Each window is completely disconnected from its relatives, and can be moved, resized, restyled (undergo font and color changes) independently of any other window on the desktop.
![]() Figure 10-1 News sample program output
The truth about the news application in this chapter is that it suffers from considerable limitations. I will explain these in detail at the end of the chapter; briefly, you will receive no capability of posting messages in this application - news is strictly a news "reader." There is also no way to print, save, or copy news articles, and no way to keep track of what you have viewed and what articles you have not seen before. It sounds as if this program is virtually useless, but in reality it will show you the basic interface to news servers, and demonstrate some advanced features that you will find in few other news programs. I will leave the extensions as an exercise for you. If you remember the basic concepts of object orientation, you will have little difficulty attaching new features to this program to suit your own requirements. Most of the code you will find in this news example has been discussed in detail in previous chapters, so we will skip those portions of the code which have been reused. There are, however, lots of new things to learn in this chapter, and we will start by developing a connection manager for our news client.
As explained in Chapter 7, the inherent problem with interfacing an OS/2 application to TCP/IP is that the program can multitask, while the network connection cannot. The program could make a network request and put the whole application on hold while it waits for the server to respond, but this would result in the user sitting idle for long periods of time. The alternative is to make several connections to the server so that multiple network tasks can occur in parallel. For example, with three connections to the server, one could be loading the list of available newsgroups, which is a lengthy task, while the user could be reading articles on another, and still another connection could be updating subscriptions. In theory you could make many connections and the user would never have to wait for any significant amount of time. Realize, however, that there are limitations on network bandwidth and on the server itself. The administrator of the server will likely limit the total number of concurrent connections, and the throughput you can achieve depends very much upon the rate at which your network runs. A 14.4K SLIP or PPP data link would offer little benefit in supporting 30 news server connections. Through experimentation, I have discovered that in most environments two or three server connections is sufficient for satisfactory client performance. Now that you understand what multitasking and multiconnection applications can do, you are possibly questioning how this goal can be achieved. The solution, of course, is to break the network interface out of the program mainstream and create a new class to manage all server interaction.
![]() Figure 10-2 Connection manager functionality
When the multithreaded client requires a connection, it requests a free one from the connection manager, which finds the first idle connection and returns it to the requester to accomplish its task. Once the requesting thread is finished with the connection, it informs the connection manager, and the server connection is returned to the connection pool to wait use for another task. In the event that a thread of the news client requests a connection and they are all busy, the connection manager makes the client thread wait by setting a semaphore to trigger when the connection becomes idle. The connection manager class is illustrated in Figure 10-3.
Figure 10-3 C_CONNECT_MGR class
The constructor used in the news code is shown below; all it does is create an event semaphore which is used to detect when all the connections are busy.
//------------- // Constructor \ //--------------------------------------------------------------------------- C_CONNECT_MGR::C_CONNECT_MGR( void ) { // Create an event semaphore to track the state of the connection manager hSemConMgr.Create(); hSemConMgr.Open(); hSemConMgr.Post(); } The destructor for the connection manager is responsible for closing all the connections with the news server. To each one of the connection instances, the destructor issues a call to the Close() method, which destroys the connection and disposes of the TCP/IP socket used for communications. For more detail on the process involved in Close(), look at the C_CONNECT_NEWS class previously implemented in Chapter 7.
//------------ // Destructor \ //--------------------------------------------------------------------------- C_CONNECT_MGR::~C_CONNECT_MGR( void ) { int iCtr; // Open each connection iCtr = iConnectionCount - 1; while( iCtr >= 0 ) { // Close the server connection (pxcConnect + iCtr )->Close(); iCtr--; } // Get rid of the network object delete pxcConnect; } Creating an instance of the connection manager using the void constructor is not sufficient to establish connections to the news server. The Initialize method provides this functionality. Initialize() accepts a specified number of connections and a server address, as well as a TCP/IP port to use for the communications. The method creates an instance of the C_CONNECT_NEWS class for each connection, initializes the connection object, and attempts to open each connection with the server. Finally, using an attribute internal to C_CONNECT_MGR, Initialize() sets the activity state of each connection to an idle indicator.
//------------ // Initialize \ //--------------------------------------------------------------------------- // // Description: // This method establishes a connection instance for each of the // required server connections and initializes each of them. // // Parameters: // iConCount - Number of server connections to create // szServer - Address of the news server // iPort - TCP/IP port number to use for connections // // Returns: // void // void C_CONNECT_MGR::Initialize( int iConCount, char *szServer, int iPort ) { int iResult; int iCtr; // Save the attributes iConnections = iConCount; iConnectionCount = 0; strcpy( szNewsServer, szServer ); iNewsPort = iPort; // Create the specified number of connection instances pxcConnect = (C_CONNECT_NEWS *)new C_CONNECT_NEWS[iConnections]; // Initialize each connection for( iCtr = 0; iCtr < MaxConnections(); iCtr++ ) { // Initialize the news server connection (pxcConnect+iCtr)->Initialize( Server(), Port() ); } // Open each connection for( iCtr = 0; iCtr < MaxConnections(); iCtr++ ) { // Open the connection iResult = (pxcConnect+iCtr)->Open(); if( iResult >= D_NET_OK ) { // Say that we have a connection open IncrementConnectionCount(); } // Indicate that the connection is initially idle (pxcConnect+iCtr)->Busy( 0 ); } } The connection manager code permits requesting threads to close a connection. The Close() method sends a "close" message to the connection object and marks the state of the connection as idle. Use of this method is not recommended unless you are sure you know what you are doing. Closing a connection outside the connection manager will prevent any other thread from accessing that connection. It is used mainly for error control in the client code - if the news client determines that a problem exists on a specific connection, it can be shut down and still allow the other connections to continue operating normally.
//------- // Close \ //--------------------------------------------------------------------------- // // Description: // This method closes the specified news server connection. // // Parameters: // iConnection - Connection to close // // Returns: // void // void C_CONNECT_MGR::Close( int iConnection ) { // Close the connection (pxcConnect+iConnection)->Close(); // Indicate that it is not longer busy (pxcConnect+iConnection)->Busy( 0 ); // Reduce the number of available connections if( iConnectionCount > 0 ) iConnectionCount--; } When the client application needs to access the server, it calls the C_CONNECT_MGR::Connect() method to request a connection. This member function waits for a free connection, if none is available, by referencing the semaphore assigned to this duty. Assuming a connection is idle, its offset number is returned to the caller and the connection is marked as "busy." If all connections are currently busy, the method will loop at half-second intervals until a connection becomes available. This is a potential problem point in the code; though, after many hours of testing, I was not able to hang the program in this code, it should be approached with caution.
//--------- // Connect \ //--------------------------------------------------------------------------- // // Description: // This method returns connection from the idle connection pool to the // caller. The connection is marked as busy, and the calling function is // responsible for disconnecting when its task is complete. // // Parameters: // none // // Returns: // int - >=0 connection for use. <0 no connection available // int C_CONNECT_MGR::Connect( void ) { int iCtr; ULONG lCount; // Wait for a free connection hSemConMgr.WaitIndefinite(); hSemConMgr.Reset( &lCount ); do { // Look at each connection for( iCtr = 0; iCtr < iConnections; iCtr++ ) { // Is the connection busy? if( (pxcConnect+iCtr)->Socket() && !(pxcConnect+iCtr)->Busy() ) { // Not busy, so we'll use it. Mark as busy. (pxcConnect+iCtr)->Busy( 1 ); // Return the connection number that was connected hSemConMgr.Post(); return iCtr; } } // Wait around a while until a connection becomes free DosSleep( 500 ); } while( 1 == 1 ); // The program should never get here, but return something to keep the // compiler happy return -1; } The requesting code can determine the number of idle connections in the connection manager using the FreeConnections() method. This is useful if you want to build a thread that uses more than one connection to "gang up" on a task. In NeoLogic News, I used this type of code to allow several connections to update subscriptions, while leaving one connection free to read articles or perform other tasks. To accomplish this, I monitored the number of free connections using FreeConnections() to make sure there was always one free for other user demands.
//----------------- // FreeConnections \ //--------------------------------------------------------------------------- // // Description: // This method returns the number of currently idle connections. // // Parameters: // none // // Returns: // int - The number of free connections. // int C_CONNECT_MGR::FreeConnections( void ) { int iCtr; int iFreeCtr; iFreeCtr = 0; // Check each connection for( iCtr = 0; iCtr < iConnections; iCtr++ ) { // If the connection is idle, count it. if( (pxcConnect+iCtr)->Socket() && !(pxcConnect+iCtr)->Busy() ) iFreeCtr++; } // Return the number of free connections return iFreeCtr; } Since a thread cannot keep a connection allocated forever, it must have some procedure in place to return the connection to the connection manager's free pool. The Disconnect() method implements this capability by simply marking the connection's operational state back to an "idle."
//------------ // Disconnect \ //--------------------------------------------------------------------------- // // Description: // This method returns the specified connection to the idle connection // pool for use by other processes. // // Parameters: // iConnect - Connection to return to the pool // // Returns: // none // void C_CONNECT_MGR::Disconnect( int iConnect ) { // Mark the connection as idle (pxcConnect+iConnect)->Busy( 0 ); } The header file for the connection manager code also contains a number of simple inline functions that mostly return class attribute values.
C_CONNECT_NEWS *Connection( int iConnect ) { return pxcConnect+iConnect; }; int FreeConnections( void ); char *Server( void ) { return szNewsServer; }; int Port( void ) { return iNewsPort; }; int Connection( void ) { return iConnectionCount; }; int MaxConnections( void ) { return iConnections; }; void IncrementConnectionCount( void ) { iConnectionCount++; }; The connection manager is now behind us. I hope you've sifted through the source because it is the key to making the news client more efficient than most similar applications, and it demonstrates some very important advantages of OS/2 over other environments such as Windows (except NT). In the next section you will see how the connection manager layer is integrated into the news client, and the advantages of the connection manager will become clear.
Now that we have established a method for communicating with a news server, we can begin to create a real applications to read news. The first step is obviously the creation of a main() procedure and, since we want to make our news client a Presentation Manager application, we need to create a main window. Most of the code for the main window need not be reviewed here, since it is quite similar to the enhanced editor and Ping which we built earlier. I will, however, explain some of the new features we have not seen previously. One of the key features of the news client is that all activity in the program is controlled by the C_WINDOW_MAIN class. This means that in many cases the main window object simply acts as a message relay. For example, when the user subscribes to a new group, the available groups window sends a message to the main window object, which in turn sends a message to the subscription window. This technique simplifies the overall design and helps with program maintenance. If you do find a problem, you can usually track it down to a message handler in C_WINDOW_MAIN, since interaction between the other windows is nonexistent. Figure 10-4 contains a message diagram that graphically explains how the program interacts with the user and its various internal parts. You should be able to use this diagram when referencing the C_WINDOW_MAIN source that follows. For the sake of simplicity, the diagram does not show every message passed. In particular, the WM_CLOSE messages have been eliminated in step 6.
![]() Figure 10-4 News message flow
//------------- // Global Data \ //--------------------------------------------------------------------------- C_APPLICATION xcApp; // Application instance C_WINDOW_MAIN xcWindow; // Main window instance C_CONNECT_MGR *pxcMgr; // Connection Manager Instance C_LOG *pxcLog; // Debug log instance Since the entire source for news is included on the companion disk, I'm not going to drag you through every line, especially since much of the code is similar to that used in previous chapters. However, we will look at some of the key components starting with the main() procedure.
void main( int argc, char *argv[] ) { // Create a debugging log file pxcLog = (C_LOG *)new C_LOG( "news.log", 1 ); pxcLog->Open(); // Register the application window xcWindow.Register( "OS/2 News" ); xcWindow.WCF_Icon(); xcWindow.WCF_SysMenu(); xcWindow.WCF_Menu(); xcWindow.WCF_TitleBar(); xcWindow.WCF_MinButton(); xcWindow.WCF_TaskList(); xcWindow.WCF_DialogBorder(); xcWindow.Create( ID_WINDOW, "OS/2 News" ); // Create the news connections pxcLog->Write( "Main:Creating Connection Manager" ); pxcMgr = (C_CONNECT_MGR *)new C_CONNECT_MGR; pxcLog->Write( "Main:Created Connection Manager" ); // Set the news server xcWindow.SetServer( argv[1] ); // Start the message loop xcApp.Run(); // Destroy the news connections delete pxcMgr; // Close and free the debug log pxcLog->Write( "NEWS:Closing Log" ); pxcLog->Close(); delete pxcLog; } The first two lines of code in main() are something we have not seen in the previous applications. These lines set up a debugging log file called NEWS.LOG, which will receive a dump of some key process information for news. These lines create an instance of a debug log and open it.
// Create a debugging log file pxcLog = (C_LOG *)new C_LOG( "news.log", 1 ); pxcLog->Open(); At the end of the main() procedure, we reverse this process by closing the log and destroying the instance.
// Close and free the debug log pxcLog->Write( "NEWS:Closing Log" ); pxcLog->Close(); delete pxcLog; The Write() operation writes the enclosed string to the log file. This is useful for determining why an application is malfunctioning. Note that C_LOG::Write() functions much like the C printf() procedure, in that you can enclose print formatting to display the values of variables used in the application.
pxcLog->Write( "The value of x is %ld", x ); Before the application message loop is started, main() also does something else that we have not seen before. It executes the following line:
// Create the news connections pxcMgr = (C_CONNECT_MGR *)new C_CONNECT_MGR; This line of code creates an instance of the connection manager that we examined in the previous section. The program has not yet connected to the server - all we have done is reserve space for the connection manager. Immediately before the message loop is started, we send the main window a server address.
// Set the news server xcWindow.SetServer( argv[1] ); The news program requires that a server IP or domain name string be specified on the command line. This string is sent to the main window, which has the responsibility for connecting to the server. The news program currently does not provide any form of error checking to ensure that an address has been added to the command line. I will leave this for you to correct. Once the program drops out of the message loop, main() resumes control and needs to remove the dynamic memory used by the connection manager. It makes the following call to destroy the connection manager object and free up its memory.
// Destroy the news connections delete pxcMgr; The main window for news is an object based on the PMCLASS C_WINDOW_STD class; from our past experience we know that we need to supply a message table, shown below. The methods it references need not be reviewed here; although I will go into detail for some of them, the majority are copies of what we have already seen in previous applications.
//--------------------------- // Main Window Message Table \ //--------------------------------------------------------------------------- DECLARE_MSG_TABLE( xtMsgMain ) DECLARE_MSG( PM_CREATE, C_WINDOW_MAIN::MsgCreate ) DECLARE_MSG( PM_GROUP_CLOSE, C_WINDOW_MAIN::MsgGroupClose ) DECLARE_MSG( PM_GROUP_SUBSCRIBE, C_WINDOW_MAIN::MsgGroupSubscribe ) DECLARE_MSG( PM_SUB_CLOSE, C_WINDOW_MAIN::MsgSubscriptionClose ) DECLARE_MSG( PM_SUB_READ, C_WINDOW_MAIN::MsgSubscriptionRead ) DECLARE_MSG( PM_MSG_CLOSE, C_WINDOW_MAIN::MsgMessageClose ) DECLARE_MSG( PM_MSG_READ, C_WINDOW_MAIN::MsgMessageRead ) DECLARE_MSG( PM_ART_CLOSE, C_WINDOW_MAIN::MsgArticleClose ) DECLARE_MSG( PM_CONNECT, C_WINDOW_MAIN::MsgConnect ) DECLARE_MSG( WM_CLOSE, C_WINDOW_MAIN::MsgClose ) DECLARE_MSG( WM_SIZE, C_WINDOW_MAIN::MsgSize ) DECLARE_MSG( WM_CONTROL, C_WINDOW_MAIN::MsgControl ) DECLARE_MSG( WM_PAINT, C_WINDOW_STD::MsgPaint ) END_MSG_TABLE C_WINDOW_MAIN also supports some menu and toolbar commands, so we have also implemented a command table.
//--------------------------- // Main Window Command Table \ //--------------------------------------------------------------------------- DECLARE_COMMAND_TABLE( xtCommandMain ) The constructor for C_WINDOW_MAIN assigns the message and command tables and additionally initializes the attributes used by the instance.
//------------- // Constructor \ //--------------------------------------------------------------------------- // // Description: // This constructor initializes the main window class for the editor. // It zeroes the class attributes and sets up the command handler and // server address. // // Parameters: // szServer - Address of the news server // C_WINDOW_MAIN::C_WINDOW_MAIN( void ) : C_WINDOW_STD( xtMsgMain ) { // Initialize all child objects pxcTBar = 0; pxcStatus = 0; pxcMenu = 0; pxcGroups = 0; pxcSubs = 0; pxcMsg = 0; pxcArticle = 0; // Enable the required command handler for this window CommandTable( xtCommandMain ); // Set the server address strcpy( szServerAddress, "" ); } The destructor for the main window object is similar to the one we implemented for the enhanced editor. It simply deallocates the space used by all the child window objects.
//------------ // Destructor \ //--------------------------------------------------------------------------- // // Description: // The destructor frees up all the dynamically allocated objects // and attributes used by this instance. // C_WINDOW_MAIN::~C_WINDOW_MAIN( void ) { pxcLog->Write( "~C_WINDOW_MAIN:Start" ); delete pxcTBar; delete pxcStatus; delete pxcMenu; delete pxcGroups; delete pxcSubs; delete pxcMsg; delete pxcArticle; pxcLog->Write( "~C_WINDOW_MAIN:End" ); } The MsgCreate() is called when the window receives a WM_CREATE message and sets up the characteristics of the window. Since there is a lot of new code in this method, we should spend some time studying it.
//----------- // MsgCreate \ //--------------------------------------------------------------------------- // Event: WM_CREATE // Cause: Issued by OS when window is created // Description: This method gets called when the window is initially created. // It initializes all the visual aspects of the class. // void *C_WINDOW_MAIN::MsgCreate( void *mp1, void *mp2 ) { char szX[10]; char szY[10]; // Create a status bar to display miscellaneous data pxcStatus = (C_STATUS *) new C_STATUS( this ); // Create a toolbar control pxcTBar = (C_TOOLBAR_TOP *)new C_TOOLBAR_TOP( this, pxcStatus ); // Keep track of the main menu so we can enable/disable items pxcMenu = (C_MENU *)new C_MENU( this ); // Load parameters out of the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); xcIni.Read( "MainX", szX, "0", 10 ); xcIni.Read( "MainY", szY, "0", 10 ); xcIni.Close(); // Make the window look like a control panel SetSizePosition( atoi( szX ), atoi( szY ), xcApp.DesktopWidth() / 10 * 4, xcApp.DialogBorderHeight() * 2 + xcApp.TitleBarHeight() + xcApp.MenuHeight() + 65 ); // Make the window visible Show(); // Disable menu options so the user can't select information // until we're connected pxcMenu->DisableItem( DM_WINDOWS ); // Disable the toolbar buttons until we're connected pxcTBar->ButtonEnable( DB_WND_GRP, FALSE ); pxcTBar->ButtonEnable( DB_WND_SUB, FALSE ); // Begin a thread to open all the news connections xcConnectThread.Create( ConnectThread, 40000, this ); return FALSE; } The first new code we see are the lines that load the previous window states from the OSUSR.INI file. MsgMain() creates an instance of C_INI_USER and opens it in order to retrieve the last known X,Y coordinates of the main control panel. Since the size of this window is fixed, we do not need to save the width and height dimensions.
// Load parameters out of the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); xcIni.Read( "MainX", szX, "0", 10 ); xcIni.Read( "MainY", szY, "0", 10 ); xcIni.Close(); Something else that is new is the code to disable the "Windows" menu item, and the toolbar buttons used to display the available groups window and the subscription window. Since both of these windows require network access, we need to prevent the user from opening these windows until we are connected to the news server.
// Disable menu options so the user can't select information // until we're connected pxcMenu->DisableItem( DM_WINDOWS ); // Disable the toolbar buttons until we're connected pxcTBar->ButtonEnable( DB_WND_GRP, FALSE ); pxcTBar->ButtonEnable( DB_WND_SUB, FALSE ); Finally, the MsgCreate() method starts a thread to initialize the connection manager.
// Begin a thread to open all the news connections xcConnectThread.Create( ConnectThread, 40000, this ); The ConnectThread() thread function calls the Initialize() method for the connection manager instance and displays the connection status in the main window's status line. If the connection manager fails to establish a server connection, the program does not allow further processing to occur, and the user's only option is to exit the program. Assuming a successful connection was established to the server, the thread issues a PM_CONNECT message to the main window.
//--------------- // ConnectThread \ //--------------------------------------------------------------------------- // // Description: // This thread function creates a connection to the news server. If // successful, it sends a PM_CONNECT message back to the main window class. // void _Optlink ConnectThread( void *pvData ) { C_WINDOW_MAIN *pxcThis; C_THREAD_PM *pxcThread; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_MAIN *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); pxcThis->Status()->Text( "Connecting to %s...", pxcThis->ServerAddress() ); pxcMgr->Initialize( D_MAX_CONNECT, pxcThis->ServerAddress(), D_NEWS_PORT ); // If there was an error, tell the user about it if( pxcMgr->Connections() <= 0 ) { // Tell the user that the connection failed WinMessageBox( HWND_DESKTOP, pxcThis->Window(), "News could not connect to the server", "News", 0, MB_OK | MB_ICONHAND ); pxcThis->Status()->Text( "Not connected" ); } else { // Tell the main window that there were no errors - we're online! pxcThis->PostMsg( PM_CONNECT, 0, 0 ); } // Terminate the thread pxcThread->TerminateThread(); } This PM_CONNECT message is managed by the MsgConnect() method. This method enables the "Windows" menu item and toolbar buttons, and then issues a DM_SUBSCRIPTION command to display the subscription window.
//------------ // MsgConnect \ //--------------------------------------------------------------------------- // Event: PM_CONNECT // Cause: Issued by the connection thread when a connection has been // established // void *C_WINDOW_MAIN::MsgConnect( void *mp1, void *mp2 ) { // Tell the user he is connected pxcStatus->Text( "Connected to %s...", ServerAddress() ); // Enable menu options so the user can select information pxcMenu->EnableItem( DM_WINDOWS ); // Enable the toolbar buttons pxcTBar->ButtonEnable( DB_WND_GRP, TRUE ); pxcTBar->ButtonEnable( DB_WND_SUB, TRUE ); // Give the user some audible feedback to say that we've connected DosBeep( 100, 100 ); // Since we are now connected, open the subscription window and tell // it to update. SendMsg( WM_COMMAND, (void *)DM_SUBSCRIPTIONS, 0 ); return FALSE; } When the DM_SUBSCRIPTION command is received, the CmdSubscriptions() method is called. It creates an instance of the subscription window class and sends it a PM_POPULATE message to begin loading the subscriptions.
//------------------ // CmdSubscriptions \ //--------------------------------------------------------------------------- // Event: DM_SUBSCRIPTION // Cause: User selects the Window/Subscriptions option in order to // display the list of available groups supported by the server. // void *C_WINDOW_MAIN::CmdSubscriptions( void *mp1, void *mp2 ) { // Only permit the user to open a sub window if it isn't already open if( !pxcSubs ) { // Create a new instance of the groups window pxcSubs = (C_WINDOW_SUBSCRIPTION *)new C_WINDOW_SUBSCRIPTION; // Set up groups window pxcSubs->Register( "Subscriptions" ); pxcSubs->WCF_Standard(); pxcSubs->Create( ID_SUBSCRIPTIONS, "Current Subscriptions" ); // Tell the group window that this is its parent pxcSubs->SendMsg( PM_PARENT, (void *)this, 0 ); // Tell the group window to populate pxcSubs->SendMsg( PM_POPULATE, 0, 0 ); } else { // Otherwise give the user focus to the existing window pxcSubs->Show(); } return FALSE; } If the user closes the subscription window, a PM_SUB_CLOSE message is sent to the main window to notify it of the action. The instance of the subscription window is then destroyed.
//---------------------- // MsgSubscriptionClose \ //--------------------------------------------------------------------------- // Event: PM_SUB_CLOSE // Cause: Issued by the subscription window when the user closes it // void *C_WINDOW_MAIN::MsgSubscriptionClose( void *mp1, void *mp2 ) { // Delete the current subscription window delete pxcSubs; pxcSubs = 0; return FALSE; } When the user clicks the "groups" toolbar button or menu selection, the groups windows is created. This invokes the CmdGroups() method, which creates a new instance of the groups window class and issues a PM_POPULATE message to it. This code is almost a duplicate of the subscription startup code.
//----------- // CmdGroups \ //--------------------------------------------------------------------------- // Event: DM_GROUPS // Cause: User selects the Window/Groups option in order to display the // list of available groups supported by the server. // void *C_WINDOW_MAIN::CmdGroups( void *mp1, void *mp2 ) { // Only permit the user to open a groups window if it isn't already open if( !pxcGroups ) { // Create a new instance of the groups window pxcGroups = (C_WINDOW_GROUP *)new C_WINDOW_GROUP; // Set up the available groups window pxcGroups->Register( "Groups" ); pxcGroups->WCF_Standard(); pxcGroups->Create( ID_GROUPS, "Available Newsgroups" ); // Tell the group window that this is its parent pxcGroups->SendMsg( PM_PARENT, (void *)this, 0 ); // Tell the group window to populate pxcGroups->SendMsg( PM_POPULATE, 0, 0 ); } else { // Otherwise give the user focus to the existing window pxcGroups->Show(); } return FALSE; } Like the subscription window, when the group window is closed it sends a message back to the main window manager. The PM_GROUP_CLOSE message causes the MsgGroupClose() method to be activated, which destroys the group window instance.
//--------------- // MsgGroupClose \ //--------------------------------------------------------------------------- // Event: PM_GROUP_CLOSE // Cause: Issued by the group window when the user closes it // void *C_WINDOW_MAIN::MsgGroupClose( void *mp1, void *mp2 ) { // Close and Delete the current group window delete pxcGroups; pxcGroups = 0; return FALSE; } The groups window has an additional purpose that generates a different window message. The groups window permits the user to select new newsgroups to which a subscription will be made. For each group that the user subscribes to, the group window will send a PM_GROUP_SUBSCRIBE message. Though the main window is not responsible for subscribing to newsgroups, it is the central control for the application. The group window knows nothing about the subscription process, so the main window acts as a messenger. When a PM_GROUP_SUBSCRIBE message is received in the message queue, the main window generates a PM_SUB_SUBSCRIBE for the subscription window, attaching the names of the new groups.
//------------------- // MsgGroupSubscribe \ //--------------------------------------------------------------------------- // Event: PM_GROUP_SUBSCRIBE // Cause: Issued by the group window when the user subscribes to a group. // Description: This method is invoked any time a group is subscribed to. // The mp1 parameter contains a pointer to the group name string // being subscribed. // void *C_WINDOW_MAIN::MsgGroupSubscribe( void *mp1, void *mp2 ) { // Tell the subscription window about the new group pxcSubs->SendMsg( PM_SUB_SUBSCRIBE, mp1, 0 ); return FALSE; } If the user selects a group from the subscription, this notifies news that the user wishes to read the list of subjects from that group. The subscription window sends a PM_SUB_READ message to the main window handler. MsgSubscriptionRead() determines the name of the group to be read, creates an instance of a message list window, and instructs this new window to populate itself with information from the server.
//---------------------- // MsgSubscriptionRead \ //--------------------------------------------------------------------------- // Event: PM_SUB_READ // Cause: Issued by the subscription window when the user wants to // read a subscription. // Description: This method is called any time the user selects a subscription // to read. This will create an instance of a message list window // and display the subscription contents. // void *C_WINDOW_MAIN::MsgSubscriptionRead( void *mp1, void *mp2 ) { char *szGroup; char szString[64]; // Determine which group is being displayed szGroup = (char *)mp1; pxcLog->Write( "MsgSubscriptionRead:Group=%s", szGroup ); // Only permit the user to open a groups window if it isn't already open if( !pxcMsg ) { // Create a new instance of the groups window pxcMsg = (C_WINDOW_MESSAGE *)new C_WINDOW_MESSAGE; // Set up message window pxcMsg->Register( "Message" ); pxcMsg->WCF_Standard(); pxcMsg->Create( ID_MESSAGES, "Message List" ); // Tell the message window that this is its parent pxcMsg->SendMsg( PM_PARENT, (void *)this, 0 ); // Tell the message window to populate for the given group in mp1 pxcMsg->SendMsg( PM_POPULATE, mp1, 0 ); } else { // Otherwise give the user focus to the existing window pxcMsg->Show(); } return FALSE; } When the user closes a message window, a PM_MSG_CLOSE message is sent to the main window that invokes the MsgMessageClose() method. This destroys the instance of the message list window and deallocates the memory it was using.
//----------------- // MsgMessageClose \ //--------------------------------------------------------------------------- // Event: PM_MSG_CLOSE // Cause: Issued by the message window when the user closes it // void *C_WINDOW_MAIN::MsgMessageClose( void *mp1, void *mp2 ) { // Delete the current message window delete pxcMsg; pxcMsg = 0; return FALSE; } If the user selects a subject from the message list, news is notified that an article should be displayed. This starts the MsgMessageRead() method, which manages creation of an article window and instructs the new window to load a specific article number requested by the message list window when the item was selected.
//---------------- // MsgMessageRead \ //--------------------------------------------------------------------------- // Event: PM_MSG_READ // Cause: Issued by the message window when the user wants to read // an article. // Description: This method is called any time the user selects an article // to read. This will create an instance of an article window // and display the article supplied in mp1 to the viewer. // void *C_WINDOW_MAIN::MsgMessageRead( void *mp1, void *mp2 ) { pxcLog->Write( "MsgMessageRead:File=%s", (char *)mp1 ); // Only permit the user to open a window if it isn't already open if( !pxcArticle ) { // Create a new instance of the groups window pxcArticle = (C_WINDOW_ARTICLE *)new C_WINDOW_ARTICLE; // Set up article window pxcArticle->Register( "Article" ); pxcArticle->WCF_Standard(); pxcArticle->Create( ID_ARTICLE, "News Article" ); // Tell the message window that this is its parent pxcArticle->SendMsg( PM_ART_PARENT, (void *)this, 0 ); // Tell the article window to populate for the given article in mp1 pxcArticle->SendMsg( PM_ART_POPULATE, mp1, 0 ); } else { // Otherwise give the user focus to the existing window pxcArticle->Show(); } return FALSE; } As you have probably come to expect by now, when the user closes an article viewer window, a message is sent back to the main window handler. PM_ART_CLOSE causes the MsgArticleClose() method to be called, which destroys the article window instance.
//----------------- // MsgArticleClose \ //--------------------------------------------------------------------------- // Event: PM_ART_CLOSE // Cause: Issued by the article window when the user closes it // void *C_WINDOW_MAIN::MsgArticleClose( void *mp1, void *mp2 ) { // Delete the current article window delete pxcArticle; pxcArticle = 0; return FALSE; } The final method we are going to examine in the C_WINDOW_MAIN class is MsgClose(), which is called when the user closes the main window by either double-clicking the mouse on the system menu button or by selecting the "Exit" menu item.
//---------- // MsgClose \ //--------------------------------------------------------------------------- // Event: WM_CLOSE // Cause: Issued by OS when window is closed // void *C_WINDOW_MAIN::MsgClose( void *mp1, void *mp2 ) { char szString[80]; int iX; int iY; int iW; int iL; // Get all the savable parameters GetSizePosition( &iX, &iY, &iW, &iL ); // Save parameters into the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); sprintf( szString, "%d", iX ); xcIni.Write( "MainX", szString ); sprintf( szString, "%d", iY ); xcIni.Write( "MainY", szString ); xcIni.Close(); // Debug pxcLog->Write( "NEWS:WM_CLOSE:Start" ); // If there is a groups window open, close it and destroy if( pxcGroups ) pxcGroups->SendMsg( WM_CLOSE, 0, 0 ); // If there is a subscriptions window open, close it and destroy if( pxcSubs ) pxcSubs->SendMsg( WM_CLOSE, 0, 0 ); // If there is a message window open, close it and destroy if( pxcMsg ) pxcMsg->SendMsg( WM_CLOSE, 0, 0 ); // If there is an article window open, close it and destroy if( pxcArticle ) pxcArticle->SendMsg( WM_CLOSE, 0, 0 ); // Application was told to close, so post a QUIT message to the OS PostMsg( WM_QUIT, 0, 0 ); // Debug pxcLog->Write( "NEWS:WM_CLOSE:End" ); return FALSE; } MsgClose() first retrieves the current size of the window and writes this information to the OS2USER.INI files. In this way, if the user has moved the window, it can be repositioned the next time the program is executed.
// Get all the savable parameters GetSizePosition( &iX, &iY, &iW, &iL ); // Save parameters into the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); sprintf( szString, "%d", iX ); xcIni.Write( "MainX", szString ); sprintf( szString, "%d", iY ); xcIni.Write( "MainY", szString ); xcIni.Close(); Finally, MsgClose() ensures that all the child windows are closed. In the current version of news, only a single instance of each window can exist. This makes management of the closure much easier for the purposes of demonstration. In a full news application, you would want to permit the user to have multiple message lists and article viewers open at any time. Closing the application would then become much more complicated because the program would be required to keep track of each window.
In this section, we will add the capability of displaying a list of available newsgroups supported by the NNTP server. We need not review every method in the C_WINDOW_GROUP class because you have seen most of this code before. Instead, I will show only those items that differ from previous examples. From the message diagram shown in Figure 10-4, we already know a great deal about the contents of the C_WINDOW_GROUP class. For example, we know that is it instantiated by the main window when the user selects the "Available Groups" menu item of the toolbar button. We also know that, when the user closes the group window, it issues a PM_GRP_CLOSE message to the instance of C_WINDOW_MAIN. Finally, if the user subscribes to a group, we know that this window generates a PM_GRP_SUBSCRIBE message. The basic functions are all implemented in the message and command methods of the class. The message table for C_WINDOW_GROUP follows.
//---------------------------- // Group Window Message Table \ //--------------------------------------------------------------------------- // DECLARE_MSG_TABLE( xtMsgGroup ) DECLARE_MSG( PM_CREATE, C_WINDOW_GROUP::MsgCreate ) DECLARE_MSG( PM_PARENT, C_WINDOW_GROUP::MsgParent ) DECLARE_MSG( WM_CLOSE, C_WINDOW_GROUP::MsgClose ) DECLARE_MSG( WM_SIZE, C_WINDOW_GROUP::MsgSize ) DECLARE_MSG( WM_CONTROL, C_WINDOW_GROUP::MsgControl ) DECLARE_MSG( WM_PAINT, C_WINDOW_STD::MsgPaint ) DECLARE_MSG( PM_POPULATE, C_WINDOW_GROUP::MsgPopulate ) END_MSG_TABLE The C_WINDOW_GROUP class currently provides only two command handlers. These are shown in the following command table.
//---------------------------- // Group Window Command Table \ //--------------------------------------------------------------------------- // DECLARE_COMMAND_TABLE( xtCommandGroup ) DECLARE_COMMAND( DM_GROUP_SUBSCRIBE, C_WINDOW_GROUP::CmdSubscribe ) DECLARE_COMMAND( DM_GROUP_LOAD, C_WINDOW_GROUP::CmdRefresh ) END_MSG_TABLE The constructor and destructor for this class should look familiar by now. The constructor simply associates the instance with the message and command tables, while the destructor frees up the dynamic memory allocated by the child window objects when the instance was created.
//------------- // Constructor \ //--------------------------------------------------------------------------- // // Description: // This constructor assigns the message and command tables for this class. // C_WINDOW_GROUP::C_WINDOW_GROUP( void ) : C_WINDOW_STD( xtMsgGroup ) { // Enable the required handlers for this window CommandTable( xtCommandGroup ); } //------------- // Destructor \ //--------------------------------------------------------------------------- // // Description: // This destructor disposes of the memory used by the child // window classes. // C_WINDOW_GROUP::~C_WINDOW_GROUP( void ) { pxcLog->Write( "GROUP:Destructor:Start" ); // Free up the child windows delete pxcTBar; delete pxcStatus; delete pxcCont; pxcLog->Write( "GROUP:Destructor:End" ); } The MsgCreate() message handler retrieves any saved window size and position information for the group windows, as well as the previous window colors and font. It uses this data to restore the window to the exact state it was in during the previous execution of the program.
//----------- // MsgCreate \ //--------------------------------------------------------------------------- // Event: WM_CREATE // Cause: Issued by OS when window is created // void *C_WINDOW_GROUP::MsgCreate( void *mp1, void *mp2 ) { char szX[10]; char szY[10]; char szW[10]; char szL[10]; char szFont[80]; char szFontSize[10]; char szBColor[80]; char szFColor[80]; // Create a status bar to display miscellaneous data pxcStatus = (C_STATUS *) new C_STATUS( this ); // Create a toolbar control pxcTBar = (C_TOOLBAR_GRP *)new C_TOOLBAR_GRP( this, pxcStatus ); // Create a new container to display group listing pxcCont = (C_CONTAINER_GRP *)new C_CONTAINER_GRP( this ); // Load parameters out of the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); xcIni.Read( "GroupFont", szFont, "System Proportional", 80 ); xcIni.Read( "GroupFontSize", szFontSize, "10", 10 ); xcIni.Read( "GroupBColor", szBColor, "000,000,000", 80 ); xcIni.Read( "GroupFColor", szFColor, "255,255,255", 80 ); xcIni.Read( "GroupX", szX, "0", 10 ); xcIni.Read( "GroupY", szY, "0", 10 ); xcIni.Read( "GroupW", szW, "0", 10 ); xcIni.Read( "GroupL", szL, "0", 10 ); xcIni.Close(); // Set the font in the window pxcCont->SetFont( szFont, atoi( szFontSize ) ); // Set the window colors pxcCont->SetForegroundColor( atoi( &szFColor[0] ), atoi( &szFColor[4] ), atoi( &szFColor[8] ) ); pxcCont->SetBackgroundColor( atoi( &szBColor[0] ), atoi( &szBColor[4] ), atoi( &szBColor[8] ) ); if( atoi( szW ) != 0 && atoi( szL ) != 0 ) { // Position and size the window SetSizePosition( atoi( szX ), atoi( szY ), atoi( szW ), atoi( szL ) ); } // Make the window visible pxcCont->Focus(); return (void *)TRUE; } By examining the code in the main window, we know that when a new instance of C_WINDOW_GROUP is created, the main window sends it a PM_POPULATE message. Processing of this message is managed by the MsgPopulate() method responsible for populating the group window with the list of available newsgroups. Since loading a container control can be a very time-consuming task, MsgPopulate() begins executing a new thread to avoid blocking Presentation Manager.
//------------- // MsgPopulate \ //--------------------------------------------------------------------------- // Event: PM_POPULATE // Cause: Issued by OS during the initial creation of the groups window // // This method populates the container object within the group window. Since // this is a time-consuming task, it will be done on a separate PM thread. // void *C_WINDOW_GROUP::MsgPopulate( void *mp1, void *mp2 ) { // Begin a thread to populate the group list xcPopulateThread.Create( PopulateGroupThread, 40000, this ); return FALSE; } The PopulateGroupThread() function is executed on a separate thread to load the container information. This is the first time we have seen any interaction with the server, so I'll spend a bit more time on this code. The first thing that the thread function does is to determine if the group file needs to be retrieved from the server. Since this list can contain in excess of 5000 groups for a typical server, it can take a significant amount of time to download. For this reason the list of available groups is maintained in the GROUPS.GRP. If this file does not exist, it must be downloaded from the server. In the PopulateGroupThread() code you will see the following code:
// Get a network connection iConnection = pxcMgr->Connect(); if( iConnection >= 0 ) { pxcMgr->Connection(iConnection)->List( "groups.grp" ); pxcMgr->Disconnect( iConnection ); } This code retrieves a free connection from the connection manager, and uses this connection to fetch a list of all available newsgroups from the server. This information is stored in the GROUPS.GRP file. The final step in the network task is to return the connection back to the connection manager by calling the -Disconnect() method. Once the GROUPS.GRP file exists, it is opened and each item is counted. This is done to improve the performance of the container load. Based on the item count, enough memory is allocated from the heap to store each container record. Then the GROUPS.GRP file is reopened and each of the group strings is loaded into a container record, using the following code:
pRecord = (T_GRPRECORD *)pxcThis->Container()->Fill( pRecord, szString ); The code for the container load, and more specifically the Fill() method, will be described in more detail later in this chapter. After each record has been populated, the records are inserted into the container window by placing a call to the Insert() method of the group container class.
// Perform the container insertion pxcThis->Container()->Insert( 0, pFirstRecord, iCount ); Finally, the records are sorted and the container window is redrawn to show the list of available newsgroups.
// Sort the records pxcThis->Container()->Sort( SortGroupByAlpha ); That is all there is to loading a container. It is not a very difficult concept to grasp as long as you remember that the container code within Presentation Manager is not very fast, so this code should always run on its own thread of execution. The listing for the PopulateGroupThread() function follows.
//---------------- // PopulateThread \ //--------------------------------------------------------------------------- // // Description: // This thread loads the groups from the groups.grp file into the // container displayed by the group window. This is done on a separate // thread because it can be a very time-consuming task. // void _Optlink PopulateGroupThread( void *pvData ) { C_WINDOW_GROUP *pxcThis; C_THREAD_PM *pxcThread; char cChar; char szString[1024]; FILE *hFile; int iCount; int iConnection; T_GRPRECORD *pFirstRecord; T_GRPRECORD *pRecord; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_GROUP *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Look to see if the group file is here if( access( "groups.grp", 0 ) != 0 ) { pxcThis->Status()->Text( "Loading groups from server..." ); // Debug pxcLog->Write( "Loading groups from the server" ); // Get a network connection iConnection = pxcMgr->Connect(); // Debug pxcLog->Write( "Got connection:%d", iConnection ); if( iConnection >= 0 ) { pxcLog->Write( "Getting list" ); pxcMgr->Connection(iConnection)->List( "groups.grp" ); pxcLog->Write( "Disconnecting" ); pxcMgr->Disconnect( iConnection ); } pxcLog->Write( "Done loading groups from the server" ); } // The first thing we need to do is determine the number of // supported groups pxcThis->Status()->Text( "Populating news groups..." ); iCount = 0; hFile = fopen( "groups.grp", "r" ); if( hFile ) { while( !feof( hFile ) && fgets( szString, 1024, hFile ) ) { // Get rid of any CR/LF if( strstr( szString, "\r" ) ) *strstr( szString, "\r" ) = 0; if( strstr( szString, "\n" ) ) *strstr( szString, "\n" ) = 0; // Get the support indicator character cChar = (char)toupper( szString[strlen( szString ) - 1] ); // If this group is supported count it if( cChar != 'X' || ( cChar == 'X' && szString[strlen( szString ) - 2] != ' ' ) ) { iCount++; } } fclose( hFile ); } pxcLog->Write( "iCount =%d", iCount ); // Insert groups if there are any if( iCount > 0 ) { // Allocate some container space, but keep track of where it starts pFirstRecord = (T_GRPRECORD *)pxcThis->Container()->Allocate( sizeof( T_GRPRECORD ), (USHORT)(iCount+1) ); pRecord = pFirstRecord; // Now insert each supported record into the container iCount = 0; hFile = fopen( "groups.grp", "r" ); while( !feof( hFile ) && fgets( szString, 1024, hFile ) ) { // Get rid of any CR/LF if( strstr( szString, "\r" ) ) *strstr( szString, "\r" ) = 0; if( strstr( szString, "\n" ) ) *strstr( szString, "\n" ) = 0; // Get the support indicator character cChar = (char)toupper( szString[strlen( szString ) - 1] ); // If this group is supported count it if( cChar != 'X' || ( cChar == 'X' && szString[strlen( szString ) - 2] != ' ' ) ) { if( strstr( szString, " " ) ) *strstr( szString, " " ) = 0; // Insert this record if( strlen( szString ) ) { pRecord = (T_GRPRECORD *)pxcThis->Container()->Fill( pRecord, szString ); iCount++; } } } fclose( hFile ); // Perform the container insertion pxcThis->Container()->Insert( 0, pFirstRecord, iCount ); // Sort the records pxcThis->Container()->Sort( SortGroupByAlpha ); // Tell the user how many groups there are in the list pxcThis->Status()->Text( "%d Groups loaded", iCount ); } // Terminate the thread pxcThread->TerminateThread(); } The last window message handler we will look at for C_WINDOW_GROUP is the MsgClose() method called by the window manager to close the group window. The code should be relatively obvious; the only thing I will mention is the presence of a line to send a message back to the news control panel window. This PM_GROUP_CLOSE message simply notifies the owner window that the groups window no longer exists.
//---------- // MsgClose \ //--------------------------------------------------------------------------- // Event: WM_CLOSE // Cause: Issued by OS when window is closed // void *C_WINDOW_GROUP::MsgClose( void *mp1, void *mp2 ) { char szString[80]; int iX; int iY; int iW; int iL; BYTE byR; BYTE byG; BYTE byB; // Get all the savable parameters GetSizePosition( &iX, &iY, &iW, &iL ); // Save parameters into the INI file C_INI_USER xcIni( "BookNews" ); xcIni.Open(); // Save the window dimensions sprintf( szString, "%d", iX ); xcIni.Write( "GroupX", szString ); sprintf( szString, "%d", iY ); xcIni.Write( "GroupY", szString ); sprintf( szString, "%d", iW ); xcIni.Write( "GroupW", szString ); sprintf( szString, "%d", iL ); xcIni.Write( "GroupL", szString ); // Save the font pxcCont->GetFont( szString ); if( strstr( szString, "." ) ) { xcIni.Write( "GroupFont", strstr( szString, "." ) + 1 ); *strstr( szString, "." ) = 0; xcIni.Write( "GroupFontSize", szString ); } // Save the window foreground color pxcCont->GetForegroundColor( &byR, &byG, &byB ); sprintf( szString, "%03d,%03d,%03d", byR, byG, byB ); xcIni.Write( "GroupFColor", szString ); // Save the window background color pxcCont->GetBackgroundColor( &byR, &byG, &byB ); sprintf( szString, "%03d,%03d,%03d", byR, byG, byB ); xcIni.Write( "GroupBColor", szString ); xcIni.Close(); // Kill any threads we own that are still running xcPopulateThread.Kill(); // Commit suicide Destroy(); // Tell the parent that the user told us to shut down. Our parent will // clean up our mess (i.e. call our destructor) pxcParent->PostMsg( PM_GROUP_CLOSE, 0, 0 ); return FALSE; } The C_WINDOW_GROUP class supports two commands from the user. The first of these manages new subscriptions. The user is free to select one or more groups from the available groups list and subscribe to them. When the Subscribe option is selected by the user, CmdSubscribe() uses the FirstSelected() method in the container class to determine if any groups have been chosen. For each selected group, this method sends a PM_GROUP_SUBSCRIBE message to the main news window, then deselects the item. CmdSubscribe() then uses the container method, NextSelected(), to find any additional group selections, and repeats the subscription process, if required.
//-------------- // CmdSubscribe \ //--------------------------------------------------------------------------- // Event: DM_SUBSCRIBE // Cause: User selects the subscribe option from the toolbar or menu // Description: This method subscribes to all the currently highlighted // container items. It does this by sending a PM_SUBSCRIBE // message to its parent window (the news control panel). // void *C_WINDOW_GROUP::CmdSubscribe( void *mp1, void *mp2 ) { T_GRPRECORD *pRecord; // Get the first selected record in the list pRecord = (T_GRPRECORD *)pxcCont->FirstSelected(); while( pRecord ) { pxcLog->Write( "pRecord = %s", pRecord->szString ); // Tell our parent that the user wants to subscribe to this group pxcParent->SendMsg( PM_GROUP_SUBSCRIBE, (void *)pRecord->szString, 0 ); // Unselect as we go so the user knows something is happening pxcCont->SelectRecord( pRecord, FALSE ); // Go to the next record pRecord = (T_GRPRECORD *)pxcCont->NextSelected( pRecord ); } return FALSE; } The second command handler is responsible for refreshing the group list. CmdRefresh() removes any existing GROUPS.GRP file. Then, by issuing a PM_POPULATE message to the message manager for the instance, this method forces the list to be retrieved from the server.
//------------ // CmdRefresh \ //--------------------------------------------------------------------------- // Event: DM_REFRESH // Cause: User selects the refresh option from the toolbar or menu // Description: This method removes the group file from the disk and forces // the news server to resend it. // void *C_WINDOW_GROUP::CmdRefresh( void *mp1, void *mp2 ) { // Force the groups file to go away DosForceDelete( "groups.grp" ); // Say that we want to reload the groups from the server PostMsg( PM_POPULATE, 0, 0 ); return FALSE; }
In this section, we will add the code for the subscription window to news. This window keeps track of what groups the user has an interest in, and displays the number of messages in each. Since so much of the source code for the subscription window is similar to that found in the group window, we need only discuss a few key parts. However, the entire source for this window can be found on the companion disk. The message table for the C_WINDOW_SUBSCRIPTION class is almost identical to that shown previously for C_WINDOW_GROUP.
//----------------------------------- // Subscription Window Message Table \ //--------------------------------------------------------------------------- DECLARE_MSG_TABLE( xtMsgSubscription ) DECLARE_MSG( PM_CREATE, C_WINDOW_SUBSCRIPTION::MsgCreate ) DECLARE_MSG( PM_PARENT, C_WINDOW_SUBSCRIPTION::MsgParent ) DECLARE_MSG( PM_POPULATE, C_WINDOW_SUBSCRIPTION::MsgPopulate ) DECLARE_MSG( PM_SUB_SUBSCRIBE, C_WINDOW_SUBSCRIPTION::MsgSubSubscribe ) DECLARE_MSG( WM_CLOSE, C_WINDOW_SUBSCRIPTION::MsgClose ) DECLARE_MSG( WM_SIZE, C_WINDOW_SUBSCRIPTION::MsgSize ) DECLARE_MSG( WM_CONTROL, C_WINDOW_SUBSCRIPTION::MsgControl ) DECLARE_MSG( WM_PAINT, C_WINDOW_STD::MsgPaint ) END_MSG_TABLE The command table for the class includes two entries. The function of both of these methods is described later in this section.
//----------------------------------- // Subscription Window Command Table \ //--------------------------------------------------------------------------- DECLARE_COMMAND_TABLE( xtCommandSub ) DECLARE_COMMAND( DM_SUB_UNSUBSCRIBE, C_WINDOW_SUBSCRIPTION::CmdSubUnsubscribe ) DECLARE_COMMAND( DM_SUB_READ, C_WINDOW_SUBSCRIPTION::CmdSubRead ) END_MSG_TABLE The constructor and destructor for C_WINDOW_SUBSCRIBE are identical to those outlined for the groups window. The MsgPopulate() message handler is also similar, so I won't go into any further detail for it either, except to note that it loads the subscription window's container by starting the Populate-Subscription-Thread() function on a separate thread of execution. PopulateSubscriptionThread() loads the contents of the GROUPS.SUB file into the container window, and does so in a manner similar to the technique used for loading the list of available groups. Once each record is loaded and displayed, this thread scans each group and, with the help of the server, populates the total number of articles stored in each newsgroup. The code establishes a connection with the connection manager layer, and then issues a GROUP command to the server to fetch the total number of articles. This value is stored in the record for each group, and the container window is updated.
//---------------- // PopulateThread \ //--------------------------------------------------------------------------- // // Description: // This thread function is used to populate the subscription window // container. It loads data from the subscription file, parses it, and // writes it to the container window. // void _Optlink PopulateSubscriptionThread( void *pvData ) { C_WINDOW_SUBSCRIPTION *pxcThis; C_THREAD_PM *pxcThread; char szString[1024]; FILE *hFile; int iConnection; int iCount; T_SUBRECORD *pFirstRecord; T_SUBRECORD *pRecord; ULONG lFirst; ULONG lLast; ULONG lTotal; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_SUBSCRIPTION *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Remove any previous data because this could be an update pxcThis->Container()->RemoveAll(); // The first thing we need to do is determine the number of // supported groups iCount = 0; hFile = fopen( "groups.sub", "rt" ); if( hFile ) { while( !feof( hFile ) && fgets( szString, 1024, hFile ) ) { // Found a subscription iCount++; } fclose( hFile ); } pxcLog->Write( "SUBS:iCount =%d", iCount ); // Insert groups if there are any if( iCount > 0 ) { // Allocate some container space, but keep track of where it starts pFirstRecord = (T_SUBRECORD *)pxcThis->Container()->Allocate( sizeof( T_SUBRECORD ), (USHORT)iCount ); pRecord = pFirstRecord; // Now insert each supported record into the container hFile = fopen( "groups.sub", "rt" ); while( !feof( hFile ) && fgets( szString, 1024, hFile ) ) { // Get rid of any CR/LF if( strstr( szString, "\r" ) ) *strstr( szString, "\r" ) = 0; if( strstr( szString, "\n" ) ) *strstr( szString, "\n" ) = 0; // Insert this record pRecord = (T_SUBRECORD *)pxcThis->Container()->Fill( pRecord, szString ); } fclose( hFile ); // Perform the container insertion pxcThis->Container()->Insert( 0, pFirstRecord, iCount ); // Redraw the container to show the subscriptions pxcThis->Container()->Redraw( 0 ); // Get a network connection iConnection = pxcMgr->Connect(); if( iConnection >= 0 ) { // Update the group information pRecord = (T_SUBRECORD *)pxcThis->Container()->FirstRecord(); while( pRecord ) { // Get the group information and article status pxcMgr->Connection(iConnection)->Group( pRecord->szGroup, &lFirst, &lLast, &lTotal ); // Update the record and redraw it sprintf( pRecord->szTotal, "%ld", lTotal ); pxcThis->Container()->Redraw( pRecord ); // Step to the next record pRecord = (T_SUBRECORD *)pxcThis-> Container()->NextRecord( pRecord ); } } // Free up the network connection pxcMgr->Disconnect( iConnection ); } // Terminate the thread pxcThread->TerminateThread(); } The other thread implemented in the subscription window code manages removal of groups from the subscription window. Users may wish to stop reading a group, so the subscription window provides an "Unsubscribe" capability that triggers the UnsubscribeThread() function to be executed on a separate thread. This code scans the subscription container and removes the desired group. It then rewrites the GROUPS.SUB subscription file to exclude the removed group.
//------------------- // UnsubscribeThread \ //--------------------------------------------------------------------------- // // Description: // This thread is responsible for unsubscribing to newsgroups. It // removes the group(s) from the subscription container and rewrites // the groups.sub subscription file. // void _Optlink UnsubscribeThread( void *pvData ) { C_WINDOW_SUBSCRIPTION *pxcThis; C_THREAD_PM *pxcThread; FILE *hFile; T_SUBRECORD *pRecord; T_SUBRECORD *pNext; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_SUBSCRIPTION *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Update the group information pRecord = (T_SUBRECORD *)pxcThis->Container()->FirstSelected(); while( pRecord ) { // Get the next record pNext = (T_SUBRECORD *)pxcThis->Container()->NextSelected( pRecord ); // Remove the record pxcThis->Container()->Remove( pRecord ); // Set the current record equal to the next pRecord = pNext; } // Refresh the container view pxcThis->Container()->Redraw( 0 ); // Look for the first container item pRecord = (T_SUBRECORD *)pxcThis->Container()->FirstRecord(); if( pRecord ) { // Update the subscription file hFile = fopen( "groups.sub", "wt" ); if( hFile ) { // Loop to every subscription in the container while( pRecord ) { // Write the item to the subscription file fprintf( hFile, "%s\n", pRecord->szGroup ); // Get the next record pRecord = (T_SUBRECORD *)pxcThis->Container()-> NextRecord( pRecord ); } fclose( hFile ); } } else { // No records left, so clean up the file DosForceDelete( "groups.sub" ); } // Terminate the thread pxcThread->TerminateThread(); } C_WINDOW_SUBSCIPTION class is also responsible for adding newsgroups to the subscription list. As previously described, we know that the main window sends a PM_SUB_SUBSCRIBE message when a new group needs to be added to the list; the MsgSubSubscribe() manages this message. The code first waits to ensure that the thread that populated the list is idle. It does this by invoking a call to WaitIndefinite(), a method in the C_SEM_EVENT class. This call points out a potential design and implementation problem. Waiting for a semaphore is not the kind of thing you should normally do in a message handler. In this case, there should be no problem; however, depending on your design, the posting of the semaphore may well depend on the occurrence of some other Presentation Manager operation. This can result in deadlocking PM due to a violation of the 1/10 second rule. Take definite care when waiting for semaphores while executing on the main thread of a PM program.
//----------------- // MsgSubSubscribe \ //--------------------------------------------------------------------------- // Event: PM_SUB_SUBSCRIBE // Cause: Issued by parent window when a new subscription is sent; // mp1 contains a pointer to the group string being subscribed. // void *C_WINDOW_SUBSCRIPTION::MsgSubSubscribe( void *mp1, void *mp2 ) { char *szGroup; FILE *hFile; // Wait for any current subscription update to complete xcPopulateThread.WaitIndefinite(); // Get the group that is being subscribed szGroup = (char *)mp1; pxcLog->Write( "PM_SUB_SUBSCRIBE:%s", szGroup ); // Write the new group to the subscription file hFile = fopen( "groups.sub", "at" ); if( hFile ) { pxcLog->Write( "PM_SUB_SUBSCRIBE:Writing:%d", fprintf( hFile, "%s\n", szGroup ) ); fclose( hFile ); } // Repopulate the container PostMsg( PM_POPULATE, 0, 0 ); return FALSE; } I previously described the UnsubscribeThread() function. This is activated by the CmdSubUnsubscribe() method when the user selects the "Unsubscribe" menu item or presses the toolbar button.
//------------------- // CmdSubUnsubscribe \ //--------------------------------------------------------------------------- // Event: DM_SUB_UNSUBSCRIBE // Cause: User selects the Unsubscribe button or menu option // Description: This method is called any time the user wants to unsubscribe // to newsgroup(s). The method finds each selected item and // removes it from the list. void *C_WINDOW_SUBSCRIPTION::CmdSubUnsubscribe( void *mp1, void *mp2 ) { // Begin a thread to unsubscribe all selected items xcUnsubscribeThread.Create( UnsubscribeThread, 40000, this ); return FALSE; } The second command implemented by the C_WINDOW_SUBSCRIPTION class is invoked when the user double-clicks the left mouse button while the mouse pointer is positioned over a group in the subscription list. Alternatively, the "Read" menu item or toolbar button may be selected to execute this code. When CmdSubRead() is executed, it locates the first selected subscription groups and issues a PM_SUB_READ message to the main window. This subsequently displays a list of messages for this group in the message window. This will be the subject of the next section.
//------------ // CmdSubRead \ //--------------------------------------------------------------------------- // Event: DM_SUB_READ // Cause: User selects the Read button or menu option // Description: This method is called any time the user wants to read the // message headers for a selected subscription. If more than // one subscription is selected when this command is invoked, // the first selected subscription is used and the remainder // are ignored. void *C_WINDOW_SUBSCRIPTION::CmdSubRead( void *mp1, void *mp2 ) { T_SUBRECORD *pRecord; // Get the first selected group pRecord = (T_SUBRECORD *)pxcCont->FirstSelected(); if( pRecord ) { // Tell the parent to display the message list pxcParent->SendMsg( PM_SUB_READ, pRecord->szGroup, 0 ); } return FALSE; }
In this section, I will briefly describe the process involved in displaying lists of messages for newsgroups. These lists are displayed when a group is selected from the subscription window, and consist mainly of a message title and a message number that identifies the message. The code to accomplish this window display is similar to what we have already seen for the group and subscription windows. Scan the following message table used to control the message window and you will see the commonality. The code is so similar to previous source code that you should have few problems understanding the program flow.
//------------------------------ // Message Window Message Table \ //--------------------------------------------------------------------------- DECLARE_MSG_TABLE( xtMsgMessage ) DECLARE_MSG( PM_CREATE, C_WINDOW_MESSAGE::MsgCreate ) DECLARE_MSG( PM_PARENT, C_WINDOW_MESSAGE::MsgParent ) DECLARE_MSG( PM_POPULATE, C_WINDOW_MESSAGE::MsgPopulate ) DECLARE_MSG( WM_CLOSE, C_WINDOW_MESSAGE::MsgClose ) DECLARE_MSG( WM_SIZE, C_WINDOW_MESSAGE::MsgSize ) DECLARE_MSG( WM_CONTROL, C_WINDOW_MESSAGE::MsgControl ) DECLARE_MSG( WM_PAINT, C_WINDOW_STD::MsgPaint ) END_MSG_TABLE //------------------------------ // Message Window Command Table \ //--------------------------------------------------------------------------- DECLARE_COMMAND_TABLE( xtCommandMsg ) DECLARE_COMMAND( DM_MSG_READ,C_WINDOW_MESSAGE::CmdMsgRead ) END_MSG_TABLE There are some key features of the message list window that I will describe, however. These features include loading of messages from the server; since there are two different techniques for this, it is important to understand the variations. We know, from the message diagram shown at the beginning of this chapter, that the main program window sends a PM_POPULATE to the message list window when the window is created. This invokes the MsgPopulate() method, which sets the window title to match the name of the subscription group and then creates a thread calling PopulateMessageThread() to load the message list container with information about the articles in the newsgroup.
//------------- // MsgPopulate \ //--------------------------------------------------------------------------- // Event: PM_POPULATE // Cause: Issued by the main window whenever it wants us to display a // new message. // Description: This method is invoked whenever the main window wants to // change the group for which this window is listing messages. // The mp1 parameter contains a string holding the name of the // new group. // void *C_WINDOW_MESSAGE::MsgPopulate( void *mp1, void *mp2 ) { char szString[64]; // Get the group name to populate strcpy( szGroup, (char *)mp1 ); pxcLog->Write( "MESSAGE:MsgPopulate:%s", szGroup ); // Set the window title to the first 56 characters of the group name // This is a limitation of OS/2 memset( szString, 0, 64 ); strncpy( szString, szGroup, 56 ); SetTitle( szString ); // Begin a thread to load the messages xcPopulateThread.Create( PopulateMessageThread, 40000, this ); return FALSE; } The only other message processor I am going to mention in this chapter is the CmdMsgRead() command handler. It is called whenever the user selects an item from the message list indicating that the article is to be displayed for reading. CmdMsgRead() simply starts a news thread, calling the LoadMessage-Thread() routine to load the requested message from the server and inform the main news window that it should display the article.
//------------ // CmdMsgRead \ //--------------------------------------------------------------------------- // Event: DM_MSG_READ // Cause: User selects the Read button or menu option // Description: This method is called any time the user wants to read the // article text for a specified message. // If more than one message is selected when this command is // invoked, the first selected subscription is used and the // remainder are ignored. void *C_WINDOW_MESSAGE::CmdMsgRead( void *mp1, void *mp2 ) { T_MSGRECORD *pRecord; // Get the first selected group pxcLog->Write( "MESSAGE:CmdMsgRead:Start" ); pRecord = (T_MSGRECORD *)pxcCont->FirstSelected(); if( pRecord ) { // Start a thread to load the article xcLoadThread.Create( LoadMessageThread, 40000, this ); } pxcLog->Write( "MESSAGE:CmdMsgRead:End" ); return FALSE; } The message window source code includes a thread to load the message list information from the server. PopulateMessageThread() performs similarly to the threads used to load the groups and subscriptions shown previously, but is required to implement some additional parsing to force the information into a format desirable for our client. As was described in the C_CONNECT_NEWS code, some servers support the XOVER command, which performs a quick load of the messages for a given group. Some servers, however, do not implement overview support, so a slower loading process must be used. We will look at both these loading techniques shortly. The PopulateMessageThread() code attempts to load the overview by invoking the Overview() method for the current server connection. If the process fails to create an overview file, the code calls the SlowParse() procedure; otherwise, FastParse() is used. Once the list of messages from the server has been parsed, they are counted and inserted into the container.
//----------------------- // PopulateMessageThread \ //--------------------------------------------------------------------------- // // Description: // This thread function is used to populate the message window container. // It loads data from the message file, parses it, and writes it // to the container window. // void _Optlink PopulateMessageThread( void *pvData ) { char szString[1024]; C_WINDOW_MESSAGE *pxcThis; C_THREAD_PM *pxcThread; FILE *hFile; int iCount; int iConnection; int iResult; T_MSGRECORD *pRecord; T_MSGRECORD *pFirstRecord; ULONG lStart; ULONG lLast; ULONG lTotal; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_MESSAGE *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Get a network connection iConnection = pxcMgr->Connect(); if( iConnection >= 0 ) { // Get the group information and article status iResult = pxcMgr->Connection(iConnection)->Group( pxcThis->Group(), &lStart, &lLast, &lTotal ); if( iResult >= 200 && iResult <= 300 ) { pxcThis->Status()->Text( "Loading Overview..." ); // Try to do an overview of the messages iResult = pxcMgr->Connection(iConnection)->Overview( lStart, lLast, "news.ovr" ); if( iResult >= 200 && iResult < 300 ) { // Overview supported, so do a quick parse iCount = pxcThis->FastParse(); // Get rid of the overview file DosForceDelete( "news.ovr" ); } else { // Overview not supported, so do the listing the slow way iCount = pxcThis->SlowParse( iConnection, lStart, lLast ); } } // Free up the network connection pxcMgr->Disconnect( iConnection ); // Remove any previous data because this could be an update pxcThis->Container()->RemoveAll(); // Insert groups if there are any if( iCount > 0 ) { // Allocate some container space, but keep track of where it starts pFirstRecord = (T_MSGRECORD *)pxcThis->Container()->Allocate( sizeof( T_MSGRECORD ), (USHORT)iCount ); pRecord = pFirstRecord; // Now insert each supported record into the container hFile = fopen( "news.msg", "r" ); while( !feof( hFile ) && fgets( szString, 1024, hFile ) ) { // Get rid of any CR/LF if( strstr( szString, "\r" ) ) *strstr( szString, "\r" ) = 0; if( strstr( szString, "\n" ) ) *strstr( szString, "\n" ) = 0; // Insert this record pRecord = (T_MSGRECORD *)pxcThis->Container()->Fill( pRecord, szString ); } // Perform the container insertion pxcThis->Container()->Insert( 0, pFirstRecord, iCount ); // Redraw the container to show the subscriptions pxcThis->Container()->Redraw( 0 ); fclose( hFile ); // Get rid of the message file DosForceDelete( "news.msg" ); } } // Terminate the thread pxcThread->TerminateThread(); } As mentioned earlier, the SlowParse() routine is called whenever the news client detects a server that does not support the XOVER server command. This code loops for each message that must be loaded, and issues a "HEAD" command to the server. Though the news protocol class C_CONNECT_NEWS implements a Head() method, it writes to a disk file; in order to improve performance somewhat, SlowParse() issues the "HEAD" command itself and reads the responses directly into memory. Once the command has been issued from the server, each line of the header for the requested messages is returned from the server (even lines for which we have no need). SlowParse() extracts the information it needs by examining the beginning of each line. For example, if a line starts with "Subject:" then the code determines that this is the header line containing the subject text. The output below is an actual message header extracted from Usenet, which demonstrates some of the fields processed by the parser.
Path: stn.ns.ca!newsflash.concordia.ca!utcsri!utnut!isnews.csc.calpoly.edu!usenet From: gutz@hookup.net (Steven Gutz) Newsgroups: comp.os.os2.mail-news Subject: NeoLogic Mail program ?? Date: 8 Sep 1995 20:51:07 GMT Organization: NeoLogic Inc. Lines: 17 Message-ID: <42qabr$73t@hookup.net> NNTP-Posting-Host: mailer.hookup.net X-Newsreader: NeoLogic News for OS/2 [version: 4.3] After each field is parsed, a new line is written to the message list file containing all the information displayed by the message list window. Then SlowParse() fetches the next message header, and the process repeats. It is not very difficult to see why this routine is call "SlowParse." The software must read in all sorts of useless information that needs to be parsed, and large portions of it are simply discarded. Still, until only a few years ago when some inventive person devised a fast method (XOVER), this is how every news reader retrieved message information from the server.
//----------- // SlowParse \ //--------------------------------------------------------------------------- // // Description: // This method is used to load message information from servers that // do not support the XOVER command. It loads the header for each message // using the HEAD command and parses the information into a format that // is acceptable to news. // // Parameters: // iConnection - News connection to use for acquire // lFirst - First message number in the range // lLast - Last message number in the range // // Returns: // int - The number of messages found in the specified range. // int C_WINDOW_MESSAGE::SlowParse( int iConnection, ULONG lFirst, ULONG lLast ) { char szHeader[4096]; char szAuthor[256]; char szSubject[256]; char szLines[256]; FILE *hFile; int iCount; ULONG lCtr; iCount = 0; hFile = fopen( "news.msg", "wt" ); if( hFile ) { // go to the first article lCtr = lFirst; while( lCtr <= lLast ) { // Tell the user what's going on with the loading process pxcStatus->Text( "Loading %ld of %ld", lCtr - lFirst + 1, lLast - lFirst + 1 ); // Get the header for the new article and return the result sprintf( szHeader, "head %ld\r\n", lCtr ); pxcMgr->Connection(iConnection)->Send( szHeader ); pxcMgr->Connection(iConnection)->Receive( szHeader ); if( atoi( szHeader ) >= 200 && atoi( szHeader ) < 300 ) { // Skip the path string pxcMgr->Connection(iConnection)->Receive( szHeader ); // Initial data areas memset( szAuthor, 0, 256 ); memset( szSubject, 0, 256 ); memset( szLines, 0, 256 ); while( strcmp( szHeader, "." ) != 0 ) { // Parse Lines if( strncmp( szHeader, "Lines:", 6 ) == 0 ) { strncpy( szLines, szHeader + 7, 256 ); if( strstr( szLines, "\r" ) ) *strstr( szLines, "\r" ) = 0; } if( strncmp( szHeader, "From:", 5 ) == 0 ) { strncpy( szAuthor, szHeader + 6, 256 ); if( strstr( szAuthor, "\r" ) ) *strstr( szAuthor, "\r" ) = 0; } if( strncmp( szHeader, "Subject:", 8 ) == 0 ) { strncpy( szSubject, szHeader + 9, 256 ); if( strstr( szSubject, "\r" ) ) *strstr( szSubject, "\r" ) = 0; } pxcMgr->Connection(iConnection)->Receive( szHeader ); } // Write this to the file and increment the message count sprintf( szHeader, "%ld\t%s\t%s\t%s", lCtr, szLines, szAuthor, szSubject ); fprintf( hFile, "%s\n", szHeader ); iCount++; } // Go to the next message lCtr++; } fclose( hFile ); } return iCount; } Of course, most servers do implement the XOVER command now, and it offers vast improvement over the previous technique. The news client can retrieve information from the server without needing to perform significant parsing, and none of the information retrieved is generally thrown away (though the limited news reader here does discard some data). The FastParse() routine extracts its information from the overview file, which has been fetched from the news server using the Over() method in the C_CONNECT_NEWS class. This data has a known format, which was described in Chapter 7, and field information can be extracted in a known order. Once extracted, data is written to a message list file in a format which the message window container recognizes.
//----------- // FastParse \ //--------------------------------------------------------------------------- // // Description: // This method is used to load message information from servers that // do support the XOVER command. It loads the headers in overview format // and parses the information into a format acceptable to news. // // Parameters: // none // // Returns: // int - The number of messages found in the overview file. // int C_WINDOW_MESSAGE::FastParse( void ) { char szString[2048]; char szHeader[4096]; char szAuthor[256]; char szSubject[256]; char szLines[256]; char *szPtr; FILE *hFile; FILE *hInFile; int iCount; ULONG lCtr; iCount = 0; hInFile = fopen( "news.ovr", "rt" ); if( hInFile ) { hFile = fopen( "news.msg", "wt" ); if( hFile ) { while( !feof( hInFile ) && fgets( szString, 2048, hInFile ) ) { // Get rid of any CR/LF if( strstr( szString, "\r" ) ) *strstr( szString, "\r" ) = 0; if( strstr( szString, "\n" ) ) *strstr( szString, "\n" ) = 0; // Make sure every field has a value, even if it is blank while( strstr( szString, "\t\t" ) ) { // Pad empty fields in the overview line with a space char szPtr = strstr( szString, "\t\t" ) + 1; memmove( szPtr + 1, szPtr, strlen( szPtr ) + 1 ); *szPtr = ' '; } // Initial data areas memset( szAuthor, 0, 256 ); memset( szSubject, 0, 256 ); memset( szLines, 0, 256 ); // Get next message number from overview file szPtr = strtok( szString, "\t" ); if( szPtr ) lCtr = atol( szPtr ); // Get the subject text szPtr = strtok( NULL, "\t" ); if( szPtr ) { strncpy( szSubject, szPtr, 256 ); if( strstr( szSubject, "\t" ) ) *strstr( szSubject, "\t" ) = 0; } // Get the author text szPtr = strtok( NULL, "\t" ); if( szPtr ) { strncpy( szAuthor, szPtr, 256 ); if( strstr( szAuthor, "\t" ) ) *strstr( szAuthor, "\t" ) = 0; } // Skip the date,Message-ID and Reference szPtr = strtok( NULL, "\t" ); szPtr = strtok( NULL, "\t" ); szPtr = strtok( NULL, "\t" ); szPtr = strtok( NULL, "\t" ); // Get the Line text szPtr = strtok( NULL, "\t" ); if( szPtr ) { strncpy( szLines, szPtr, 256 ); if( strstr( szLines, "\t" ) ) *strstr( szLines, "\t" ) = 0; } // Write this to the file and increment the message count sprintf( szHeader, "%ld\t%s\t%s\t%s", lCtr, szLines, szAuthor, szSubject ); fprintf( hFile, "%s\n", szHeader ); // Count the number of items written to the output file iCount++; } // Close the output file fclose( hFile ); } // Close the overview file fclose( hInFile ); } // Return the number of messages in the overview return iCount; } The second thread of execution in the message window is responsible for loading complete article text from the server. When the user determines that an article should be viewed, the desired message can be selected from the list. This initiates the LoadMessageThread(), which acquires a server connection from the connection manager and uses the C_CONNECT_NEWS::Article() member function to fetch the article into the ARTICLE.TXT file. Once the article has been successfully loaded, LoadMessageThread() sends a PM_MSG_READ message back to the main window to tell the program that the article needs to be displayed.
//------------------- // LoadMessageThread \ //--------------------------------------------------------------------------- // // Description: // This thread function is used to load the text for a message into a // disk file on the local system. Once loaded, the code sends a message // back to message to display the article in an article window. // void _Optlink LoadMessageThread( void *pvData ) { C_WINDOW_MESSAGE *pxcThis; C_THREAD_PM *pxcThread; int iConnection; int iResult; T_MSGRECORD *pRecord; ULONG lStart; ULONG lLast; ULONG lTotal; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_MESSAGE *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Find out which record is selected pRecord = (T_MSGRECORD *)pxcThis->Container()->FirstSelected(); // Get a network connection iConnection = pxcMgr->Connect(); if( iConnection >= 0 ) { // Get the group information and article status iResult = pxcMgr->Connection(iConnection)->Group( pxcThis->Group(), &lStart, &lLast, &lTotal ); if( iResult >= 200 && iResult <= 300 ) { pxcThis->Status()->Text( "Loading Article..." ); // Try to load the article iResult = pxcMgr->Connection(iConnection)->Article( atol( pRecord->szNumber ), "article.txt" ); if( iResult >= 200 && iResult < 300 ) { // Tell the parent to create an article window for this. pxcThis->Parent()->SendMsg( PM_MSG_READ, (void *)"article.txt", 0 ); } } // Free up the network connection pxcMgr->Disconnect( iConnection ); } // Terminate the thread pxcThread->TerminateThread(); } As with the code shown in the previous sections, we need not review much of this code as it is similar to what has already been described. The complete source for the message window is included in the companion disk.
In this section, I will describe how article text is displayed so the user can read it. However, I am going to show you only two routines from the article code. We have already seen most of the article code in other places. The article window uses a multiline edit control to display information, and the code for this is remarkably like that we have seen for the Enhanced System Editor in Chapter 8. In fact, the editor actually implements far more capabilities than the article window does, and you may want to transfer some of that code into this source if you are planning to enhance news. For example, the news article window does not implement clipboard operations for copying and pasting text, nor does it offer any way of saving articles to a permanent disk file. The editor, however, implements all of this and more, and you should be able to transfer those features to the article window with some simple cut-and-paste. I wanted to keep the viewer as simple as possible, so I deliberately left out these features. When the user selects an item from the message list window, the main news window creates a new instance of an article window and issues a PM_POPULATE message to the new window. This executes the MsgPopulate() method in the article code, which simply starts a new thread to load the possibly large message into the article viewer's multiline edit control.
//------------- // MsgPopulate \ //--------------------------------------------------------------------------- // Event: PM_POPULATE // Cause: Issued by OS during the initial creation of the group window // // This method populates the container object within the group window. Since // this is a time-consuming task, it will be done on a separate PM thread. // void *C_WINDOW_ARTICLE::MsgPopulate( void *mp1, void *mp2 ) { // Get the filename we're supposed to load strcpy( szArticle, (char *)mp1 ); // Begin a thread to load the article text xcPopulateThread.Create( PopulateArticleThread, 40000, this ); return FALSE; } The PopulateThread() thread function loads text from the ARTICLE.TXT file into the MLE. This is almost exactly the same as the code used in the Enhanced System Editor in Chapter 8.
//---------------- // PopulateThread \ //--------------------------------------------------------------------------- // // Description: // This thread function loads the article window's MLE control with the // text from the message file. // void _Optlink PopulateArticleThread( void *pvData ) { C_WINDOW_ARTICLE *pxcThis; C_THREAD_PM *pxcThread; // Get a point to the main window object pxcThread = (C_THREAD_PM *)pvData; pxcThis = (C_WINDOW_ARTICLE *)pxcThread->ThreadData(); // Create a PM process for this thread pxcThread->InitializeThread(); // Load the multiline editor with the article file pxcThis->MLE()->Load( pxcThis->Status(), pxcThis->Article() ); // Terminate the thread pxcThread->TerminateThread(); }
This news reader application is naturally limited in features and scope. In this section, I will quickly overview some of the missing components; in many cases, I will outline some possible solutions to fill these voids. The point of this exercise is to give you enough knowledge of the weaknesses of this program so that you can extend it to meet your own specific needs. If you are looking for features to add, an excellent place to look is in other news reader applications. Do not limit your search to OS/2 readers-there are many excellent readers available on the Windows and UNIX platforms that offer comprehensive feature sets. The first and most obvious limitation of this client is the lack of posting capability. The current news application is limited to reading news articles only. Virtually all commercial and shareware news readers allow the creation of articles, which are submitted to Usenet via the news server. Many of the newsgroups supported on Usenet carry binary files encrypted in ASCII text form. This encryption is normally achieved through the use of a uuencode program. Many news readers supply the capability of reversing the encoding process to extract the binary information, allowing users to store this information on their own systems. An easy solution to allow support for binary files is to load the articles and then transparently start a uudecode program. There are many flavors of uudecode available as freeware or shareware, any of which will suit the task. One final notable omission from news and other applications in this book is the capability of printing information to an output device, such as an ink jet or laser printer. I will not be discussing printing capability because it is beyond the scope of this book; however, there are several excellent sources of information regarding printing. One particularly useful source are the sample programs available in the OS/2 Programmer's Toolkit. One of these programs describes, in detail, the selection of print queues and page formatting. A more advanced news reader application should permit the user to print articles in a paged output format. Without this capability, the program becomes almost useless. There are, of course, many other limitations to the news reader presented here, but I will leave it as an exercise for you to determine what features you deem important. The object-oriented approach used in the current implementation should permit significant enhancement without the existing code hindering your planned extensions.
Many times throughout this chapter I noted that I was omitting parts of the source because they were similar to code found in some other class. If you are a real object-oriented programming guru, then you have to be asking some questions about the efficiency of this. Duplication of code is not supposed to occur when building C++ applications. So I thought it prudent to take some time to justify this extra code, and to offer up a possible alternative object hierarchy to eliminate all this extra code. I implemented a lot of identical code in each object in order to keep the number of objects to a minimum. By expanding on the number of classes, it is possible to simplify the coding; however, it does tend to complicate the design. I did not want to confuse new C++ programmers reading this book with a lot of very tight objects. However, there is one possible solution to help eliminate as much as 40% of the code in the new application, simply by adding some additional objects. If you look at each of the window objects present in this chapter, you will note that all of them implement certain functions. So why not create an intermediate class that implements this common code once, and derive the window classes from it? The C_WINDOW_NEWS class is derived from C_WINDOW_STD in the PMCLASS library. The new class contains all the code that has been duplicated in each of the existing classes-for example, the MsgParent() method, or the code used to read and write state information to the INI file. Much of the existing code can be pushed up into this new class and eliminated from the classes we have already seen. Adding this one simple class can reduce the size of the code significantly; though the value of this is questionable in such a small application, if you plan to expand on this program, you may want to consider implementing this additional class as well as any other classes that either reduce the complexity of the design or the code itself. If you are building commercial applications, fewer lines equate directly with few bugs. Maintenance is a very real cost that must be factored into any commercial design, and if you can increase the code efficiency by even a few lines, maintainability will improve.
In this chapter, we have built a limited but functional NNTP news reader. Though it is not nearly as complete as a typical user would require, it can very quickly access news articles from any of the newsgroups supported by a given server. Of course, many other features are required to make this a more usable application. For example, the program does not support posting new articles or replying to existing articles, but the C_CONNECT_NEWS class in the NETCLASS library does support these capabilities, so adding a posting feature should not be very difficult. With a little work, the application presented in this chapter can be extended to something functional enough for typical users. The purpose of this limited news reader is to demonstrate how to interface an advanced Presentation Manager application to TCP/IP and the Internet. To that end, the program should be functional enough to show you how to build a more advanced networking application.
|