« August 2008 | Main | October 2008 »
September 30, 2008
Always looking for good programmers...
Logos is always looking for good programmers, and we're still trying to fill the Software Developer position posted on our jobs board, so if you're interested in helping us build some amazing software, please let us know!
Posted by Ed Ball at 1:21 PM | Comments (0) | TrackBack
September 26, 2008
Displaying a Splash Screen with C++ (Part III)
This is Part III of a series on creating a splash screen application in native code. For more information, see the Introduction and the Code License.
Part III: Launching the Application
Now that the splash screen is displayed, we need to launch the actual WPF application. This part is fairly straightforward: we just need to build the path to the executable file, then call CreateProcess to execute it.
HANDLE LaunchWpfApplication()
{
// get folder of the current process
TCHAR szCurrentFolder[MAX_PATH] = { 0 };
GetModuleFileName(NULL, szCurrentFolder, MAX_PATH);
PathRemoveFileSpec(szCurrentFolder);
// add the application name to the path
TCHAR szApplicationPath[MAX_PATH];
PathCombine(szApplicationPath, szCurrentFolder, _T("App.exe"));
// start the application
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
PROCESS_INFORMATION pi = { 0 };
CreateProcess(szApplicationPath, NULL, NULL, NULL, FALSE, 0, NULL, szCurrentFolder, &si, &pi);
return pi.hProcess;
}
This method returns a handle to the launched process; we will use this in the next installment to quit the splash screen application if the WPF application exits early (presumably due to an unexpected failure). I'll also show how to create an event that the WPF application can use to signal to the splash screen application that the WPF UI has been displayed and that the splash screen should be closed.
The CreateProcess documentation says that the handles in the PROCESS_INFORMATION structure must be closed when they are no longer needed, but the PROCESS_INFORMATION documentation lets us know that they will automatically be closed when the splash screen process exits; thus, there is no need to specially track the thread handle just so that it can be closed.
Posted by Bradley Grainger at 5:09 PM | Comments (0) | TrackBack
September 25, 2008
Displaying a Splash Screen with C++ (Part II)
This is Part II of a series on creating a splash screen application in native code. For more information, see the Introduction and the Code License.
Part II: Displaying the Window
In Part I I showed how to create a HBITMAP with the splash screen image. This installment will show how to create a window that displays that image.
Each Win32 window needs a window class, and the splash screen window is no different. The window class for the splash screen window will be fairly standard, although the interesting thing is that we can use DefWindowProc as the WndProc because we don't actually need special processing for any window messages. The window class also specifies an icon because I prefer to have splash screens show up in the Alt+Tab list, and it looks better if we provide an icon. (Note that in this code, error handling has been omitted.)
// Window Class name
const TCHAR * c_szSplashClass = _T("SplashWindow");
// Registers a window class for the splash and splash owner windows.
void RegisterWindowClass()
{
WNDCLASS wc = { 0 };
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = g_hInstance;
wc.hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_SPLASHICON));
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = c_szSplashClass;
RegisterClass(&wc);
}
When creating the windows, I use an old trick (a hidden owner window) to make the splash screen appear in the Alt+Tab list, but not in the taskbar. If you wanted the splash screen to also appear in the taskbar, you could drop the hidden owner window. If you didn't want it to appear in the Alt+Tab list or the taskbar, you could use the WS_EX_TOOLWINDOW extended window style (and omit the owner window).
// Creates the splash owner window and the splash window.
HWND CreateSplashWindow()
{
HWND hwndOwner = CreateWindow(c_szSplashClass, NULL, WS_POPUP,
0, 0, 0, 0, NULL, NULL, g_hInstance, NULL);
return CreateWindowEx(WS_EX_LAYERED, c_szSplashClass, NULL, WS_POPUP | WS_VISIBLE,
0, 0, 0, 0, hwndOwner, NULL, g_hInstance, NULL);
}
The window now exists, but isn't sized or positioned correctly, and has no content. The UpdateLayeredWindow function can be used to correct all these problems at once. There are several steps to this process:
- Get the dimensions of the splash screen image
- Get the dimensions of the work area on the primary monitor
- Calculate the location of the splash screen (in order to centre it on the primary monitor)
- Create a memory DC that contains the splash image
- Specify a blend function that will use the per-pixel alpha of the splash image
- Pass all this information to UpdateLayeredWindow
// Calls UpdateLayeredWindow to set a bitmap (with alpha) as the content of the splash window.
void SetSplashImage(HWND hwndSplash, HBITMAP hbmpSplash)
{
// get the size of the bitmap
BITMAP bm;
GetObject(hbmpSplash, sizeof(bm), &bm);
SIZE sizeSplash = { bm.bmWidth, bm.bmHeight };
// get the primary monitor's info
POINT ptZero = { 0 };
HMONITOR hmonPrimary = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO monitorinfo = { 0 };
monitorinfo.cbSize = sizeof(monitorinfo);
GetMonitorInfo(hmonPrimary, &monitorinfo);
// center the splash screen in the middle of the primary work area
const RECT & rcWork = monitorinfo.rcWork;
POINT ptOrigin;
ptOrigin.x = rcWork.left + (rcWork.right - rcWork.left - sizeSplash.cx) / 2;
ptOrigin.y = rcWork.top + (rcWork.bottom - rcWork.top - sizeSplash.cy) / 2;
// create a memory DC holding the splash bitmap
HDC hdcScreen = GetDC(NULL);
HDC hdcMem = CreateCompatibleDC(hdcScreen);
HBITMAP hbmpOld = (HBITMAP) SelectObject(hdcMem, hbmpSplash);
// use the source image's alpha channel for blending
BLENDFUNCTION blend = { 0 };
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = 255;
blend.AlphaFormat = AC_SRC_ALPHA;
// paint the window (in the right location) with the alpha-blended bitmap
UpdateLayeredWindow(hwndSplash, hdcScreen, &ptOrigin, &sizeSplash,
hdcMem, &ptZero, RGB(0, 0, 0), &blend, ULW_ALPHA);
// delete temporary objects
SelectObject(hdcMem, hbmpOld);
DeleteDC(hdcMem);
ReleaseDC(NULL, hdcScreen);
}
The beauty of layered windows and the UpdateLayeredWindow function is that the splash window doesn't have to respond to WM_PAINT messages; Windows will paint it (and blend it correctly with the windows below it) by default.
Now that the splash screen is being displayed, we need to launch the actual application (and then dismiss the splash screen); the next installments will cover this.
Posted by Bradley Grainger at 7:53 AM | Comments (0) | TrackBack
September 23, 2008
Displaying a Splash Screen with C++ (Part I)
This is Part I of a series on creating a splash screen application in native code. For more information, see the Introduction and the Code License.
Part I: Creating a HBITMAP
In order to display an image on-screen, we need to have it available as a HBITMAP. The Windows Imaging Component allows us to decode a PNG image into a 32 bits-per-pixel bitmap (with an alpha channel) and extract its pixels into a DIB.
In this code, I'll assume the image is embedded in the resources of the splash screen EXE (using a statement similar to the following in the .RC file):
IDI_SPLASHIMAGE PNG splash.png
The first step is to create an IStream on the resource data. This involves loading the resource, copying its data into a memory buffer, then creating a stream on that buffer. (Note that the following code has been changed for pedagogical purposes; production code could be improved by using smart pointers for the COM interfaces and Windows handles, and by using exceptions to handle error conditions.) COM should have been initialised (by calling CoInitialize or CoInitializeEx) before calling this method.
// Creates a stream object initialized with the data from an executable resource.
IStream * CreateStreamOnResource(LPCTSTR lpName, LPCTSTR lpType)
{
// initialize return value
IStream * ipStream = NULL;
// find the resource
HRSRC hrsrc = FindResource(NULL, lpName, lpType);
if (hrsrc == NULL)
goto Return;
// load the resource
DWORD dwResourceSize = SizeofResource(NULL, hrsrc);
HGLOBAL hglbImage = LoadResource(NULL, hrsrc);
if (hglbImage == NULL)
goto Return;
// lock the resource, getting a pointer to its data
LPVOID pvSourceResourceData = LockResource(hglbImage);
if (pvSourceResourceData == NULL)
goto Return;
// allocate memory to hold the resource data
HGLOBAL hgblResourceData = GlobalAlloc(GMEM_MOVEABLE, dwResourceSize);
if (hgblResourceData == NULL)
goto Return;
// get a pointer to the allocated memory
LPVOID pvResourceData = GlobalLock(hgblResourceData);
if (pvResourceData == NULL)
goto FreeData;
// copy the data from the resource to the new memory block
CopyMemory(pvResourceData, pvSourceResourceData, dwResourceSize);
GlobalUnlock(hgblResourceData);
// create a stream on the HGLOBAL containing the data
if (SUCCEEDED(CreateStreamOnHGlobal(hgblResourceData, TRUE, &ipStream)))
goto Return;
FreeData:
// couldn't create stream; free the memory
GlobalFree(hgblResourceData);
Return:
// no need to unlock or free the resource
return ipStream;
}
Now that we have an IStream pointer to the data of the image, we can use WIC to load that image. An important step in this process is to use WICConvertBitmapSource to ensure that the image is in a 32bpp format suitable for direct conversion into a DIB. This method assumes that the input image is in the PNG format; for a splash screen, this is an excellent choice because it allows an alpha channel as well as lossless compression of the source image. (To make the splash screen image as small as possible, I highly recommend the PNGOUT compression utility.)
// Loads a PNG image from the specified stream (using Windows Imaging Component).
IWICBitmapSource * LoadBitmapFromStream(IStream * ipImageStream)
{
// initialize return value
IWICBitmapSource * ipBitmap = NULL;
// load WIC's PNG decoder
IWICBitmapDecoder * ipDecoder = NULL;
if (FAILED(CoCreateInstance(CLSID_WICPngDecoder, NULL, CLSCTX_INPROC_SERVER, __uuidof(ipDecoder), reinterpret_cast<void**>(&ipDecoder))))
goto Return;
// load the PNG
if (FAILED(ipDecoder->Initialize(ipImageStream, WICDecodeMetadataCacheOnLoad)))
goto ReleaseDecoder;
// check for the presence of the first frame in the bitmap
UINT nFrameCount = 0;
if (FAILED(ipDecoder->GetFrameCount(&nFrameCount)) || nFrameCount != 1)
goto ReleaseDecoder;
// load the first frame (i.e., the image)
IWICBitmapFrameDecode * ipFrame = NULL;
if (FAILED(ipDecoder->GetFrame(0, &ipFrame)))
goto ReleaseDecoder;
// convert the image to 32bpp BGRA format with pre-multiplied alpha
// (it may not be stored in that format natively in the PNG resource,
// but we need this format to create the DIB to use on-screen)
WICConvertBitmapSource(GUID_WICPixelFormat32bppPBGRA, ipFrame, &ipBitmap);
ipFrame->Release();
ReleaseDecoder:
ipDecoder->Release();
Return:
return ipBitmap;
}
Next is to use CreateDIBSection to allocate a DIB that can be written to directly. By setting up a BITMAPINFO structure with the right values, the DIB will be of the same format as the 32bpp BGRA image loaded by WIC, and the pixels can be copied directly from the WIC bitmap to the DIB.
// Creates a 32-bit DIB from the specified WIC bitmap.
HBITMAP CreateHBITMAP(IWICBitmapSource * ipBitmap)
{
// initialize return value
HBITMAP hbmp = NULL;
// get image attributes and check for valid image
UINT width = 0;
UINT height = 0;
if (FAILED(ipBitmap->GetSize(&width, &height)) || width == 0 || height == 0)
goto Return;
// prepare structure giving bitmap information (negative height indicates a top-down DIB)
BITMAPINFO bminfo;
ZeroMemory(&bminfo, sizeof(bminfo));
bminfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bminfo.bmiHeader.biWidth = width;
bminfo.bmiHeader.biHeight = -((LONG) height);
bminfo.bmiHeader.biPlanes = 1;
bminfo.bmiHeader.biBitCount = 32;
bminfo.bmiHeader.biCompression = BI_RGB;
// create a DIB section that can hold the image
void * pvImageBits = NULL;
HDC hdcScreen = GetDC(NULL);
hbmp = CreateDIBSection(hdcScreen, &bminfo, DIB_RGB_COLORS, &pvImageBits, NULL, 0);
ReleaseDC(NULL, hdcScreen);
if (hbmp == NULL)
goto Return;
// extract the image into the HBITMAP
const UINT cbStride = width * 4;
const UINT cbImage = cbStride * height;
if (FAILED(ipBitmap->CopyPixels(NULL, cbStride, cbImage, static_cast<BYTE *>(pvImageBits))))
{
// couldn't extract image; delete HBITMAP
DeleteObject(hbmp);
hbmp = NULL;
}
Return:
return hbmp;
}
Finally, these three functions can be put together to load the PNG image from the EXE’s resources and convert it to a HBITMAP:
// Loads the PNG containing the splash image into a HBITMAP.
HBITMAP LoadSplashImage()
{
HBITMAP hbmpSplash = NULL;
// load the PNG image data into a stream
IStream * ipImageStream = CreateStreamOnResource(MAKEINTRESOURCE(IDI_SPLASHIMAGE), _T("PNG"));
if (ipImageStream == NULL)
goto Return;
// load the bitmap with WIC
IWICBitmapSource * ipBitmap = LoadBitmapFromStream(ipImageStream);
if (ipBitmap == NULL)
goto ReleaseStream;
// create a HBITMAP containing the image
hbmpSplash = CreateHBITMAP(ipBitmap);
ipBitmap->Release();
ReleaseStream:
ipImageStream->Release();
Return:
return hbmpSplash;
}
The next installment will show how to create a layered window that can display this image.
Posted by Bradley Grainger at 7:07 AM | Comments (1) | TrackBack
September 22, 2008
Displaying a Splash Screen with C++ (Introduction)
Even the leanest and meanest WPF application has significant startup costs, especially in a cold start situation. There have been a number of posts describing how to display a splash screen as soon as possible (and a built-in method was introduced in .NET 3.5 SP1), but these solutions still incur the cost of loading a large part of the .NET Framework.
For pure speed, only native code will let you get a splash screen up as soon as the user double-clicks your application icon. (In fact, a splash screen application is so simple that you can further trim startup costs by avoiding the standard Visual C++ runtime libraries.) The following series of posts will detail the steps involved in using WIC and the UpdateLayeredWindow function to display a splash screen (with an alpha channel) using the Win32 API in C++.
Part I: Creating a HBITMAP
Part II: Displaying the Window
Part III: Launching the Application
Part IV: Dismissing the Splash Screen
Update: Stefan Olson posted a Visual C++ project containing all the code in this series (with a few extra features, including fading out the splash screen and the ability to load from any file, not just application resources).
Posted by Bradley Grainger at 4:49 PM | Comments (0) | TrackBack
Logos Code Blog License
Unless otherwise specified, the code posted on this blog is licensed (under an MIT-style license) as follows:
Copyright 2007-2008 Logos Bible Software
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Posted by Bradley Grainger at 10:02 AM | Comments (1) | TrackBack
September 19, 2008
Getting the file path of an assembly
I can never remember how to get the file path of a .NET Framework assembly, so I thought I'd post it here once and for all.
string strProgramPath = typeof(Program).Assembly.Location;
That is all.
Posted by Ed Ball at 2:15 PM | Comments (0) | TrackBack