• The Old New Thing

    Creating a window that can be resized in only one direction

    • 8 Comments

    Today's Little Program shows a window that can be resized in only one direction, let's say vertically but not horizontally.

    Start with the scratch program and make these changes:

    UINT OnNcHitTest(HWND hwnd, int x, int y)
    {
     UINT ht = FORWARD_WM_NCHITTEST(hwnd, x, y, DefWindowProc);
     switch (ht) {
     case HTBOTTOMLEFT:  ht = HTBOTTOM; break;
     case HTBOTTOMRIGHT: ht = HTBOTTOM; break;
     case HTTOPLEFT:     ht = HTTOP;    break;
     case HTTOPRIGHT:    ht = HTTOP;    break;
     case HTLEFT:        ht = HTBORDER; break;
     case HTRIGHT:       ht = HTBORDER; break;
     }
     return ht;
    }
    
    HANDLE_MSG(hwnd, WM_NCHITTEST, OnNcHitTest);
    

    We accomplish this by removing horizontal resize behavior from the left and right edges and corners. For the corners, we remove the horizontal resizing, but leave the vertical resizing. For the edges, we remove resizing entirely by reporting that the left and right edges should act like an inert border.

    Wait, we're not done yet. This handles resizing by grabbing the edges with the mouse, but it doesn't stop the user from hitting Alt+Space, followed by S (for Size), and then hitting the left or right arrow keys.

    For that, we need to handle WM_GET­MIN­MAX­INFO.

    void OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpmmi)
    {
     RECT rc = { 0, 0, 500, 0 };
     AdjustWindowRectEx(&rc, GetWindowStyle(hwnd), FALSE,
                        GetWindowExStyle(hwnd));
    
     // Adjust the width
     lpmmi->ptMaxSize.x =
     lpmmi->ptMinTrackSize.x =
     lpmmi->ptMaxTrackSize.x = rc.right - rc.left;
    }
    
    HANDLE_MSG(hwnd, WM_GETMINMAXINFO, OnGetMinMaxInfo);
    

    This works out great, except for the case of being maximized onto a secondary monitor, because we run into the mixed case of being small than the monitor in the horizontal direction, but larger than the monitor in the vertical direction.

    void OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpmmi)
    {
     RECT rc = { 0, 0, 500, 0 };
     AdjustWindowRectEx(&rc, GetWindowStyle(hwnd), FALSE,
                        GetWindowExStyle(hwnd));
    
     // Adjust the width
     lpmmi->ptMaxSize.x =
     lpmmi->ptMinTrackSize.x =
     lpmmi->ptMaxTrackSize.x = rc.right - rc.left;
    
     // Adjust the height
     MONITORINFO mi = { sizeof(mi) };
     GetMonitorInfo(MonitorFromWindow(hwnd,
                        MONITOR_DEFAULTTOPRIMARY), &mi);
     lpmmi->ptMaxSize.y = mi.rcWork.bottom - mi.rcWork.top
                        - lpmmi->ptMaxPosition.y + rc.bottom;
    }
    

    The math here is a little tricky. We want the window height to be the height of the work area of the window monitor, plus some extra goop in order to let the borders hang over the edge.

    The first two terms are easy to explain: mi.rcWork.bottom - mi.rcWork.top is the height of the work area.

    Next, we want to add the height consumed by the borders that hang off the top of the monitor. Fortunately, the window manager told us exactly how much the window is going to hang off the top of the monitor: It's in lpmmi->ptMaxPosition.y, but as a negative value since it is a coordinate that is off the top of the screen. We therefore have to negate it before adding it in.

    Finally, we add the borders that hang off the bottom of the work area.

    Yes, handling this mixed case (where the window is partly constrained and partly unconstrained) is annoying. Sorry.

  • The Old New Thing

    That's not how you start a boat

    • 9 Comments

    Tomorrow is Opening Day of the Seattle boating season. (Which, as I noted some time ago, is purely a social occasion with no legal significance.)

    One of my colleagues is not much of a boat person, but his wife is. (In fact, she's a commercial fisherman.) They were on board some fancy boat or other as it sat docked. He was up on the top deck—this being a boat so fancy that it had an upstairs and a downstairs—and as the preparations were made for heading out, his wife called out to him to start the boat, since he's up there already.

    Now, as I mentioned, my colleague isn't much of a boat person. But he figured, "These modern boats, how hard can it be? It's probably pushbutton nowadays." So he looked at the control panel and saw a bright red button. "Red button, that's probably the power button, right?"

    The button looked like a letter V with a dot in the middle, or at least that's how it was described to me.

    Immediately upon pressing the button, alarms rang on board the boat, and the Coast Guard called them on the radio.

    It turns out that the V with a dot is not the power button. It is the "man overboard" button. The dot is the person's head, and the letter V represents the two flailing arms.

  • The Old New Thing

    How does the window manager adjust ptMaxSize and ptMaxPosition for multiple monitors?

    • 2 Comments

    There is a note down in the documentation for the MIN­MAX­INFO structure:

    For systems with multiple monitors, the ptMaxSize and ptMaxPosition members describe the maximized size and position of the window on the primary monitor, even if the window ultimately maximizes onto a secondary monitor. In that case, the window manager adjusts these values to compensate for differences between the primary monitor and the monitor that displays the window.

    People ask about the nature of this "compensation" and how they should deal with it.

    When the window manager sends the WM_GET­MIN­MAX­INFO message to a top-level window, the MIN­MAX­INFO structure comes preinitialized with the following information:

    Member Initialized To
    ptMaxSize The dimensions of the primary monitor.
    ptMaxPosition The upper left corner of the window if it were maximized on the primary monitor.
    ptMinTrackSize The minimum size the user can resize the window.
    ptMaxTrackSize The maximum size the user can resize the window (the union of all monitors, basically).

    Two of these values are monitor-relative and are therefore subject to adjustment.

    ptMax­Position is easy. The point is moved to a corresponding relative position on the window's actual monitor.

    ptMax­Size is trickier. If the specified size is greater than or equal to the size of the primary monitor, then the ptMax­Size is adjusted to include the difference in size between the primary monitor and the actual monitor. In other words, if ptMax­Size is 20 pixels larger than the primary monitor, then it will be adjusted to being 20 pixels larger than the actual monitor. But if ptMax­Size does not completely cover the monitor, then its value is used as-is.

    That is what the documentation is referring to when it says "In that case." The case is "if the window ultimately maximizes onto a secondary monitor."

    There is a bit of a gotcha here: If your window is larger than the screen in one direction but not another. For example, you may have a narrow maximum width but a tall maximum height. (Think console windows.) In that case, you will need to use Monitor­From­Window to get the parameters for the actual monitor so you can set your maximum height appropriately.

    We'll dig a little deeper into this gotcha next time.

  • The Old New Thing

    How can I tell if a file is an image of some type?

    • 20 Comments

    A customer wanted to know if there is some standard way of determining from the full path to a file whether it is a photo or image. They are writing a program that transfers a bunch of files to a back-end server, and they want to treat photos differently from other files.

    You can use the Assoc­Get­Perceived­Type function to classify a file into one of a variety of categories. You can consult the document to see the full list, but it's things like "image", "audio", "video", "document". This information is obtained by studying the file extension and looking up the registered perceived type.

    Let's take the function out for a spin:

    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    #include <shlwapi.h>
    #include <stdio.h> // horrors! mixing C and C++!
    
    int __cdecl wmain(int argc, wchar_t **argv)
    {
     PERCEIVED perceived;
     PERCEIVEDFLAG flag;
     PWSTR pszType;
     if (argc > 1 && SUCCEEDED(AssocGetPerceivedType(
        PathFindExtensionW(argv[1]),
        &perceived, &flag, &pszType)))
     {
      wprintf(L"Type is %ls\n", pszType);
      if (perceived == PERCEIVED_TYPE_IMAGE) {
       wprintf(L"Hey, that's an image!\n");
      }
      CoTaskMemFree(pszType);
     }
     return 0;
    }
    

    Run this program and give a file name, or just an extension (with the dot) as the command line parameter. It will tell you the perceived type and include a special message if the type is an image.

    But let's look at the customer's question again. It's not clear whether they are trying to identify files by file format or by classification. For eaxmple, suppose the file in question is a TIFF image. The Assoc­Get­Perceived­Type function will report this as an image, because, well, it's an image. But that may not be a file format that their back-end server supports.

    • If they wanted to know whether the file is a PNG, GIF, or JPG because those are the image formats supported by their back-end server, then they need to check for those specific extensions (and possibly even sniff file contents if they are paranoid).
    • If they care only that the file represents some sort of image (possibly in a format their program does not know how to parse), because they want to, say, upload all images into a Pictures folder regardless of the image format, then they should use the Perceived Type.

    The customer thanked us for the pointer to the Assoc­Get­Perceived­Type function and especially for the clarifying remarks. "It turns out that the feature specification was not clear on the definition of 'image file', so that's something we will need to resolve ourselves. But the information you provided will definitely solve our problem, once we figure out what our problem is!"

  • The Old New Thing

    Is the atom returned by RegisterClass(Ex) a "real" atom?

    • 18 Comments

    A customer was debugging some code that calls Register­Class on a class that's already been registered. In this case, it was registered by another DLL in the same process. Normally, this wouldn't be a problem, because each DLL passes its own instance handle to Register­Class so that there are no name collisions. However, in this case, both DLLs are passing the CS_GLOBAL­CLASS flag, which means that collisions can occur after all.

    The customer found that the call to ERROR_CLASS_EXISTS, which makes sense in the case of a collision in the global class table. The code then calls Find­Atom to look up the class name, but Find­Atom fails, saying, "No such atom."

    Does this mean that the class atom created by Register­Class isn't a real atom? How can you get the atom for a window class that somebody else has registered?

    Okay, let's look at these questions in order.

    Is the atom a real atom? Well, if you ask the atom, it'll say, "I feel real to me." But remember that there are many atom tables in the system.

    • The global atom table.
    • One local atom table for each process.
    • The registered clipboard format atom table.
    • The registered window class atom table.
    • Possibly others, too.

    The Find­Atom function searches the process's local atom table, so if you go looking for a registered window class atom there, you're going to come up empty.

    There is no way to query the registered window class atom table directly. You only get indirect access to create items in it through Register­Class, and that atom can in turned by used as a synonym for the class name.

    Now, it so happens that although the Get­Class­Info function formally returns a BOOL, i.e., an integer that is zero on failure or nonzero on success, the success case of Get­Class­Info actually returns the class atom. This behavior is not documented, but the ATL library relies on it, so if the undocumented behavior changes in the future, it'll have an app compat shim to keep ATL happy.

  • The Old New Thing

    Access to a file's attributes is controlled by two things

    • 20 Comments

    We saw some time ago that permission to delete a file is granted either

    • if you have DELETE access on the file, or
    • if you have FILE_DELETE_CHILD access on the containing directory.

    File attributes behave in an analogous way.

    Permission to read a file's attributes is granted either

    • if you have FILE_READ_ATTRIBUTES access on the file, or
    • if you have FILE_LIST_DIRECTORY access on the containing directory.

    If you want the file's attributes, you could always get it by reading the directory, because one of the pieces of information you get from Find­First­File is the file attributes. Therefore, having permission to read a directory implicitly gives you permission to read the attributes of any file in that directory.

    (Note, of course, that write permission on attributes is another story.)

  • The Old New Thing

    Wow, they really crammed a lot into those 410 transistors

    • 29 Comments

    A colleague of mine pointed out that in yesterday's Seattle Times, there was an article about Moore's Law. To illustrate the progress of technology, they included some highlights, including the following piece of trivia:

    The Core 2 Duo processor with 410 transistors made its debut in 2002.

    You can see the photo and caption in the online version of the article if you go to the slide show and look at photo number three.

    This is an impressive feat. Intel managed to cram a Core 2 Duo into only an eighth as many transistors as the 6502.

    On the other hand, it does help to explain why the chip has so few registers. There weren't any transistors left!

  • The Old New Thing

    How do I extract the path to Control Panel from this shortcut so I can launch it?

    • 15 Comments

    A customer explained that they had a program that used the IShell­Link::Get­Path method to extract the program that is the target of a shortcut. They found that this didn't work for certain shortcuts, namely, shortcuts whose targets are not physical file paths.

    The one that they were specifically having trouble with was the Control Panel shortcut. For example, if you open the classic Control Panel, then drag any of the Control Panel items to the desktop, this will create a shortcut to that Control Panel item. If you view the properties on that shortcut, the Target will be grayed out instead of showing a path.

    "We want to get the target path of the shortcut so that we can launch the application. How can we get the target path from IShell­Link::Get­Path? Is there a special Windows API to get the path?"

    They can't get the target path because these are shortcuts to virtual objects. There is no target path to begin with.

    But if you look past the question to their problem, you can see that they don't need to know the path in the first place. All they want to do is launch the target application. The way to do this is simply to pass the shortcut to the Shell­Execute function. You can take this simple program as inspiration. Pass "open" as the verb and the full path to the shortcut as the file.

    As a bonus: Your program will also respect the other settings in the shortcut, like the Start In folder, the shortcut key, the preferred window state (normal, maximized, etc.), the custom application user model ID.

    And to answer the question (even though it isn't needed to solve the problem): Use the IShell­Link::Get­ID­List method to obtain the shortcut target regardless of whether it is a physical file or virtual namespace item.

  • The Old New Thing

    Why can't I have variadic COM methods?

    • 9 Comments

    COM methods cannot be variadic. Why not?

    Answer: Because the marshaler doesn't know when to stop.

    Suppose variadic COM methods were possible. And then you wrote this code:

    interface IVariadic
    {
     HRESULT Mystery([in] int code, ...);
    };
    
    IVariadic *variadic = something;
    uint32_t ipaddr;
    HRESULT hr = variadic->Mystery(9, 192, 168, 1, 1, &ipaddr);
    

    How would COM know how to marshal this function call? In other words, suppose that variadic is a pointer to a proxy that refers to an object in another process. The COM marshaler needs to take all the parameters to IVariadic::Mystery, package them up, send them to the other process, then unpack the parameters, and pass them to the implementation. And then when the implementation returns, it needs to take the return value and any output parameters, package them up, send them back to the originating process, where they are unpacked and applied to the original parameters.

    Consider, for example,

    interface IDyadic
    {
     HRESULT Enigma([in] int a, [out] int *b);
    };
    
    IDyadic *dyadic = something;
    int b;
    HRESULT hr = dyadic->Enigma(1, &b);
    

    If dyadic refers to an object in another process, the marshaler does this:

    • Allocate a block of memory containing the following information:
      • Information to identify the dyadic object in the other process,
      • the integer 1.
    • Transmit that block of memory to the other process.

    The other process receives the block of memory and does the following:

    • Use the information in the memory block to identify the dyadic object.
    • Extract the parameter 1 from the memory block.
    • Allocate a local integer variable, call it x.
    • Call dyadic->Enigma(1, &x). Let's say that the function stores 42 into x, and it returns E_PENDING.
    • Allocate a block of memory containing the following information:
      • The value E_PENDING (the HRESULT returned by dyadic->Enigma),
      • The integer 42 (the value that dyadic->Enigma stored in the local variable x).
    • Transmit that block of memory to the originating process.

    The originating process receives the block of memory and does the following:

    • Extracts the HRESULT E_PENDING.
    • Extracts the value 42.
    • Stores the value 42 into b.
    • Returns the value E_PENDING to the caller.

    Note that in order for the marshaler to do its job, it needs to know every parameter to the method, whether that parameter is an input parameter (which is sent from the originating process to the remote process), an output parameter (which is sent from the remote process to the originating process), and how to send that parameter. In our case, the parameter is just an integer, so sending it is just copying the bits, but in the more general case, the parameter could be a more complicated data structure.

    Now let's look at that variadic method again. How is the marshaler supposed to know what to do with the ...? It doesn't know how many parameters it needs to transfer. It doesn't know what types those parameters are. It doesn't know which ones are input parameters and which ones are output parameters.

    In order to know that, it would have to reverse-engineer the implementation of the IVariadic::Mystery function and figure out that the first parameter, the number 9, is a code that means that the method takes four 8-bit integers as input and outputs a 32-bit integer.

    This is a rather tall order for the client side of the marshaler, since it has to do its work without access to the other process. It would have to use its psychic powers to figure out how to package up the parameters, as well as how to unpack them afterward.

    Therefore, COM says, "Sorry, you can't do that."

    But what you can do is encode the parameters in a form that the marshaler understands. For example, you might use a counted array of VARIANTs or a SAFEARRAY. The COM folks already did the work to teach the marshaler how to, for example, decode the vt member of the VARIANT and understand that, "Oh, if the value is VT_I4, then the VARIANT contains a 32-bit signed integer."

    Bonus chatter: But wait, there is a MIDL attribute called [vararg]. You said that COM doesn't support variadic methods, but there is a MIDL keyword that says variadic right on the tin!

    Ah, but that [varargs] attribute is just a sleight of hand trick. Bceause when you say [varargs], what you're saying is, "The last parameter of this method is a SAFEARRAY of VARIANTs. A scripting language can expose this method to scripts as variadic, but what it actually does is take all the variadic parameters and store them into a SAFEARRAY, and then pass the SAFEARRAY."

    In other words, it indicates that the last parameter of the method acts like the C# params keyword.

  • The Old New Thing

    How did the scopes for the CryptProtectMemory function end up in a strange order?

    • 4 Comments

    A few weeks ago, I left an exercise: Propose a theory as to why the names and values of the scopes for the Crypt­Protect­Memory function are the way they are.

    I didn't know the answer when I posed the exercise, but I went back and dug into it.

    The Crypt­Protect­Memory function started out as an internal function back in Windows 2000, and when originally introduced, there were only two scopes: Within a process and cross-process. The Flags parameter therefore defined only a single bit, leaving the other bits reserved (must be zero). If the bottom bit was clear, then the memory was protected within a process; if the bottom bit was set, then the memory was protected across processes.

    Later, the team realized that they needed to add a third scope, the one that corresponds to CRYPT­PROTECT_SAME_LOGON. They didn't want to make a breaking change for existing callers, but they saw that they could retarget what used to be a Flags parameter as an Options parameter, and they added the new scope as a third option.

    The numeric values remained unchanged, which meant that the new function was backward-compatible with existing callers.

    Bonus chatter: Commenter sense is correct that SAME_LOGON can be used by a service while impersonating the client, however it is not the case that the scope can be larger when impersonating a remote user. The memory block returned by the Crypt­Protect­Memory function can be decrypted only on the same machine that encrypted it, and only as long as the machine has not been rebooted.

Page 1 of 448 (4,480 items) 12345»