/* ======================================================================== $Workfile: tabview.cpp $ $Date: 3/29/00 11:58p $ $Revision: 6 $ $Creator: Casey Muratori $ $Notice: (C) Copyright 2000 by Casey Muratori, All Rights Reserved. $ ======================================================================== */ /* ======================================================================== To build tabview.exe with MSVC 6.x, do this: cl /Ox /Ot tabview.cpp comctl32.lib user32.lib kernel32.lib shell32.lib /link /NODEFAULTLIB /ENTRY:WinMain /opt:ref /opt:nowin98 To use it, you can either start it without a command line parameter and drag-and-drop a file onto its window, or you can start it with the name of the file to load. You can also press F5 while using TabView to force it to re-load the file you're viewing. If you want to force TabView re-loads from inside another Win32 program, you can do this: HWND TabView = 0; while(IsWindow(TabView = FindWindowEx(0, TabView, "TabViewFrame", 0))) { SendMessage(TabView, WM_USER, 0, 0); } ======================================================================== */ /* ======================================================================== Explicit Dependencies ======================================================================== */ #include #include #define assert(Condition) if(!(Condition)) {Error(#Condition);} /* ************************************************************************ Function: Error Description: Displays an error message as a Win32 message box ************************************************************************ */ static void Error(char const * const Message) { MessageBox(0, Message, "TabView Fatal Error", MB_OK | MB_ICONERROR); } /* ************************************************************************ Function: SkipProgramName Description: Skips past the name of the program that Win32 prepends to the argument list so that only the arguments portion of the command line is left ************************************************************************ */ static char const * SkipProgramName(char const *CommandLine) { assert(CommandLine); // Skip to the first space character (we assume this indicates // the end of the program name) while(*CommandLine && (*CommandLine != ' ')) { // If we hit quotes, we have to skip to the next set of quotes // ignoring spaces that may occur in-between if(*CommandLine == '"') { ++CommandLine; while(*CommandLine && (*CommandLine != '"')) { ++CommandLine; } } ++CommandLine; } // Skip all the spaces in between the program name and the // first argument while(*CommandLine && (*CommandLine == ' ')) { ++CommandLine; } return(CommandLine); } /* ************************************************************************ Function: SkipInitialTabs Description: Skips any tabs that may occur at the beginning of a string, and returns the location of the first non-tab character and the number of tabs (as an out parameter) ************************************************************************ */ static char * SkipInitialTabs(char *String, int unsigned const SpacesPerTab, int unsigned &NumberOfTabs) { assert(String); // Count the number of spaces and tabs we see before hitting // a real character NumberOfTabs = 0; int unsigned NumberOfSpaces = 0; while(*String) { if(*String == '\t') { ++NumberOfTabs; } else if(*String == ' ') { ++NumberOfSpaces; } else { break; } ++String; } // Add the number of "spaced tabs" to the total tab count if(SpacesPerTab > 0) { NumberOfTabs += NumberOfSpaces / SpacesPerTab; } return(String); } /* ************************************************************************ Function: FindFirstNonTerminator Description: Finds the first non-terminating character in a string ************************************************************************ */ static char * FindFirstNonTerminator(char *String, int unsigned CharactersLeft) { assert(String); // Slip leading terminators while(CharactersLeft && ((*String == '\0') || (*String == '\n') || (*String == '\r'))) { ++String; --CharactersLeft; } if(!CharactersLeft) { String = 0; } return(String); } /* ************************************************************************ Function: FindAndTerminateEndOfLine Description: Guarentees that a string is terminated by a null terminator, then returns its location ************************************************************************ */ static char * FindAndTerminateEndOfLine(char *String, int unsigned CharactersLeft) { assert(String); // Find the end of the line while(CharactersLeft && (*String != '\0') && (*String != '\n') && (*String != '\r')) { ++String; --CharactersLeft; } // Terminate it if(CharactersLeft) { *String = 0; } else { // We return 0 in this case, because we were unable to find // a suitable termination point before we ran out of string String = 0; } return(String); } /* ************************************************************************ Function: AddTextItemToTreeView Description: Adds a single textual item to a TreeView control, while optionally updating a stack of previous items inserted on each level of the tree ************************************************************************ */ static void AddTextItemToTreeView(HWND TreeView, int AtLevel, int unsigned const TextLength, char const * const Text, int unsigned const LevelStackDepth = 0, HTREEITEM *LevelStack = 0) { assert(IsWindow(TreeView)); assert((LevelStackDepth == 0) || LevelStack); assert(Text); // Clip the level parameter to the maximum depth of the level stack if(AtLevel >= LevelStackDepth) { AtLevel = LevelStackDepth ? LevelStackDepth - 1 : 0; } // Prepare a TreeView item insertion command TVINSERTSTRUCT InsertData = {0}; InsertData.item.mask = TVIF_TEXT; // What's up with this cast? The data here had BETTER not be // modified by Windows - so why isn't is declared as const? InsertData.item.pszText = (LPSTR)Text; InsertData.item.cchTextMax = TextLength; // We now use a while loop to ensure that insertion goes properly. // The problem is that we can't guarentee that we are being passed // contiguous AtLevel indices (ie, we can't guarentee that a 4 was // always preceeded by a 3 or another 4). In these cases, our // LevelStack may have a 0 because we've skipped a level of the // tree. In these cases, we progressively back up (using the while // loop) until we hit a valid parent, or the root. while(LevelStack && (InsertData.hParent == 0)) { HTREEITEM Parent = AtLevel ? LevelStack[AtLevel - 1] : TVI_ROOT; if(Parent) { HTREEITEM InsertAfter = LevelStack[AtLevel]; InsertData.hInsertAfter = InsertAfter ? InsertAfter : TVI_FIRST; InsertData.hParent = Parent; } else { --AtLevel; } } // Finally, we get to add the item to the tree view control. HTREEITEM InsertedItem = (HTREEITEM) SendMessage( TreeView, TVM_INSERTITEM, 0, (LPARAM)&InsertData); // And if we've been given a LevelStack to maintain, we put // the inserted item into its place if(LevelStack) { LevelStack[AtLevel] = InsertedItem; } } /* ************************************************************************ Function: ClearListWindow Description: Removes all items from a TreeView control ************************************************************************ */ static void ClearTreeView(HWND TreeView) { SendMessage(TreeView, TVM_DELETEITEM, 0, (LPARAM)TVI_ROOT); } /* ************************************************************************ Function: SetControlRefreshing Description: Sets whether or not a particular control will refresh itself when its contents change ************************************************************************ */ static void SetControlRefreshing(HWND Control, bool const Refreshing) { assert(IsWindow(Control)); // Much thanks to Todd Laney for pointing out this message to me. // It was almost impossible to find in the docs because there // was no mention of it in the TreeView section (and my TreeView // was taking painfully long to initialize), but Todd saved // the day once again. SendMessage(Control, WM_SETREDRAW, Refreshing, 0); } /* ************************************************************************ Function: ReadFromFile Description: Reads a given number of bytes from a file ************************************************************************ */ static int unsigned ReadFromFile(HANDLE File, DWORD BytesToRead, LPVOID Buffer) { int unsigned ReturnRead = 0; DWORD BytesRead = 0; if(ReadFile(File, Buffer, BytesToRead, &BytesRead, 0)) { ReturnRead = (int unsigned)BytesRead; } return(ReturnRead); } /* ************************************************************************ Function: Copy Description: Copies the a chunk of memory from one place to another ************************************************************************ */ static void Copy(int unsigned Count, char const *Source, char *Destination) { while(Count--) { *Destination++ = *Source++; } } /* ************************************************************************ Function: Set Description: Sets a chunk of memory to a particular value ************************************************************************ */ static void Set(int unsigned Count, char Value, char *Destination) { while(Count--) { *Destination++ = Value; } } /* ************************************************************************ Function: ReadLineFromBufferredFile Description: Reads a line of text from a file while maintaining a back-buffer so that OS reads aren't too tiny to be fast ************************************************************************ */ static bool ReadLineFromBufferredFile(HANDLE File, int unsigned const BufferSize, char *Buffer, char *&Begin, char *&End) { // This routine sucks and I refuse to comment it until I rewrite // it to suck less. bool Continue = false; bool FillEntireBuffer = false; char * const BufferEnd = Buffer + BufferSize; if(Begin && (End != BufferEnd)) { Begin = FindFirstNonTerminator(End, BufferEnd - End); if(Begin) { End = FindAndTerminateEndOfLine(Begin, BufferEnd - Begin); if(End) { Continue = true; } else { int unsigned const Preserve = BufferEnd - Begin; Copy(Preserve, Begin, Buffer); Begin = Buffer; int unsigned BytesRead = 0; if(BytesRead = ReadFromFile( File, BufferSize - Preserve, Begin + Preserve)) { Set(BufferSize - (Preserve + BytesRead), 0, Begin + Preserve + BytesRead); End = FindAndTerminateEndOfLine(Buffer, BufferSize); Continue = (End != 0); } } } else { FillEntireBuffer = true; } } else { FillEntireBuffer = true; } if(FillEntireBuffer) { int unsigned BytesRead = 0; if(BytesRead = ReadFromFile(File, BufferSize, Buffer)) { Set(BufferSize - BytesRead, 0, Buffer + BytesRead); Begin = Buffer; End = FindAndTerminateEndOfLine(Buffer, BufferSize); Continue = (End != 0); } } return(Continue); } /* ************************************************************************ Function: PopulateTreeViewFromFile Description: Reads lines from a file and enters them into a tree view ************************************************************************ */ static bool PopulateTreeViewFromFile(HWND TreeView, char const * const FileName) { assert(IsWindow(TreeView)); assert(FileName); bool Populated = false; // Attempt to open the file HANDLE File = CreateFile(FileName, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); if(File != INVALID_HANDLE_VALUE) { // Create a page bank for the line-reading routines to use // (Is this too large for the stack? I don't actually want // to have to call a memory allocator unless it's 100% necessary) static char Buffer[1 << 16]; // Guarentee two null terminators at the end of the buffer // to prevent any overruns int unsigned BufferSize = sizeof(Buffer); Buffer[--BufferSize] = '\0'; Buffer[--BufferSize] = '\0'; // Create a stack of HTREEITEMs that will be used by // the TreeView insertion routine to keep track of what // the last item at every level of the tree is, thus ensuring // that the items will be entered under the right parents // for any level index. HTREEITEM LevelStack[256] = {0}; int unsigned const LevelStackDepth = sizeof(LevelStack) / sizeof(LevelStack[0]); // Read each line, and add them in sequence to the tree view char *CurrentBegin = 0; char *CurrentEnd = 0; while(ReadLineFromBufferredFile( File, sizeof(Buffer), Buffer, CurrentBegin, CurrentEnd)) { // Figure out what indent level the line should have based // on the tabbing int unsigned NumberOfTabs = 0; char *Item = SkipInitialTabs(CurrentBegin, 4, NumberOfTabs); // Add the line to the tree view AddTextItemToTreeView(TreeView, NumberOfTabs, CurrentEnd - Item, Item, LevelStackDepth, LevelStack); } CloseHandle(File); Populated = true; } return(Populated); } /* ************************************************************************ Function: SetTreeViewFromFile Description: Clears a tree view, then populates it from a file ************************************************************************ */ static bool SetTreeViewFromFile(HWND TreeView, char const * const FileName) { bool Result = false; // Turn off updating SetControlRefreshing(TreeView, false); // Clear ClearTreeView(TreeView); // Repopulate if(FileName) { Result = PopulateTreeViewFromFile(TreeView, FileName); } // Add an error item if population didn't work if(!Result) { char const * const Error = ""; AddTextItemToTreeView(TreeView, 0, sizeof(Error), Error); } // Allow updating again SetControlRefreshing(TreeView, true); return(Result); } /* ************************************************************************ Function: FrameProc Description: Win32 message handler for our top-level frame window ************************************************************************ */ LRESULT CALLBACK FrameProc(HWND Window, UINT Message, WPARAM WParam, LPARAM LParam) { LRESULT Result = 0; static HWND ListWindow = 0; static char ListFileName[MAX_PATH]; static char const *RefreshFileName = 0; switch(Message) { case WM_CREATE: { // Create our TreeView child control (who basically does // all the work in this application) ListWindow = CreateWindowEx( WS_EX_CLIENTEDGE, WC_TREEVIEW, "TabView", WS_VISIBLE | WS_CHILD | WS_BORDER | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT, 0, 0, 1, 1, Window, 0, GetModuleHandle(0), 0); // Assume the command line specifies a file to read if(IsWindow(ListWindow)) { char const * const FileName = SkipProgramName(GetCommandLine()); SetTreeViewFromFile(ListWindow, FileName); RefreshFileName = FileName; } else { // Fail creation if our ListWindow didn't construct Result = -1; } } break; case WM_DROPFILES: { // Determine the name of the file dropped on us // (note that in the case of multiple files, we only take // the first file) HDROP DropData = (HDROP)WParam; DragQueryFile(DropData, 0, ListFileName, sizeof(ListFileName) - 1); DragFinish(DropData); // Set the tree view to display the new file SetTreeViewFromFile(ListWindow, ListFileName); RefreshFileName = ListFileName; } break; case WM_SIZE: { // Find out how big our client area is RECT ClientRect; GetClientRect(Window, &ClientRect); // Set our TreeView child to occupy the space int Border = 0; SetWindowPos(ListWindow, 0, Border, Border, ClientRect.right - Border*2, ClientRect.bottom - Border*2, SWP_NOZORDER); } break; case WM_USER: { // If we ever get a USER message, we refresh the data // (since it's probably some custom app trying to get // us to refresh) SetTreeViewFromFile(ListWindow, RefreshFileName); } break; case WM_NOTIFY: { // If anyone presses F5 in the list view, we refresh // the data NMHDR *Header = (LPNMHDR)LParam; assert(Header); if(Header->hwndFrom == ListWindow) { if(Header->code == TVN_KEYDOWN) { NMTVKEYDOWN *KeyDown = (NMTVKEYDOWN *)Header; if(KeyDown->wVKey == VK_F5) { SetTreeViewFromFile(ListWindow, RefreshFileName); } } } } break; default: { // For 99% of our messages, the default procedure will do Result = DefWindowProc(Window, Message, WParam, LParam); } break; } return(Result); } /* ************************************************************************ Function: WinMain Description: The entry point - creates the window, populates it with lines from the target file, and then waits for the user to close the window ************************************************************************ */ int APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR CommandLine, int) { int Result = -1; // Initialize the common controls library so we can use a TreeView INITCOMMONCONTROLSEX CommonControls = {sizeof(CommonControls)}; CommonControls.dwICC = ICC_TREEVIEW_CLASSES; if(InitCommonControlsEx(&CommonControls)) { // Create a window class for a framing window that will hold // the TreeView control (initially I used the TreeView as a top-level // window, but some parts of the TreeView don't redraw correctly // when it is used as such - thus, a frame window was required) WNDCLASSEX FrameWindowClass = {sizeof(FrameWindowClass)}; FrameWindowClass.style = 0; FrameWindowClass.lpfnWndProc = FrameProc; FrameWindowClass.hInstance = GetModuleHandle(0); FrameWindowClass.hbrBackground = GetSysColorBrush(COLOR_3DFACE); FrameWindowClass.lpszMenuName = 0; FrameWindowClass.lpszClassName = "TabViewFrame"; FrameWindowClass.hIcon = 0; FrameWindowClass.hCursor = 0; FrameWindowClass.hIconSm = 0; // Let Windows know about the frame class if(RegisterClassEx(&FrameWindowClass)) { // Create the frame window HWND Frame = CreateWindowEx( WS_EX_ACCEPTFILES, FrameWindowClass.lpszClassName, "TabView - A Small but Conscientious Utility " "by Casey Muratori", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, GetModuleHandle(0), 0); if(IsWindow(Frame)) { // Show the window ShowWindow(Frame, SW_SHOW); // Process messages until someone closes the frame window MSG Message; while((IsWindow(Frame)) && ((Result = GetMessage(&Message, 0, 0, 0)) > 0)) { TranslateMessage(&Message); DispatchMessage(&Message); } } else { Error("Unable to initialize windows"); } } else { Error("Unable to initialize window classes"); } } else { Error("Unable to initialize common controls"); } return(Result); }