// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // // Copyright (c) Microsoft Corporation. All rights reserved #include "ListViewSample.h" /****************************************************************** * * * WinMain * * * * Application entrypoint * * * ******************************************************************/ int WINAPI WinMain( HINSTANCE /* hInstance */, HINSTANCE /* hPrevInstance */, LPSTR /* lpCmdLine */, int /* nCmdShow */ ) { // Ignoring the return value because we want to continue running even in the // unlikely event that HeapSetInformation fails. HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0); if (SUCCEEDED(CoInitialize(NULL))) { // Allocate ListViewApp on the heap since it is a big object. ListViewApp *pApp = new (std::nothrow) ListViewApp; if (pApp) { if (SUCCEEDED(pApp->Initialize())) { pApp->RunMessageLoop(); } delete pApp; } CoUninitialize(); } return 0; } /****************************************************************** * * * ListViewApp::ListViewApp constructor * * * * Initialize member data * * * ******************************************************************/ ListViewApp::ListViewApp() : m_d2dHwnd(NULL), m_parentHwnd(NULL), m_pD2DFactory(NULL), m_pWICFactory(NULL), m_pDWriteFactory(NULL), m_pRT(NULL), m_pTextFormat(NULL), m_pBlackBrush(NULL), m_pBindContext(NULL), m_pBitmapAtlas(NULL), m_numItemInfos(0), m_scrollRange(0), m_currentScrollPos(0), m_previousScrollPos(0), m_animatingItems(0), m_animatingScroll(0) { } /****************************************************************** * * * ListViewApp::~ListViewApp destructor * * * * Tear down resources * * * ******************************************************************/ ListViewApp::~ListViewApp() { SafeRelease(&m_pD2DFactory); SafeRelease(&m_pWICFactory); SafeRelease(&m_pDWriteFactory); SafeRelease(&m_pRT); SafeRelease(&m_pTextFormat); SafeRelease(&m_pBlackBrush); SafeRelease(&m_pBindContext); SafeRelease(&m_pBitmapAtlas); } /****************************************************************** * * * ListViewApp::Initialize * * * * Create application window and device-independent resources. * * * ******************************************************************/ HRESULT ListViewApp::Initialize() { HRESULT hr = S_OK; hr = CreateDeviceIndependentResources(); if (SUCCEEDED(hr)) { // Create parent window { //register window class WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = ListViewApp::ParentWndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = sizeof(LONG_PTR); wcex.hInstance = HINST_THISCOMPONENT; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = NULL; wcex.lpszMenuName = NULL; wcex.lpszClassName = L"DemoAppWindow"; RegisterClassEx(&wcex); // Create the application window. // // Because the CreateWindow function takes its size in pixels, we // obtain the system DPI and use it to scale the window size. FLOAT dpiX; FLOAT dpiY; m_pD2DFactory->GetDesktopDpi(&dpiX, &dpiY); m_parentHwnd = CreateWindow( L"DemoAppWindow", L"D2D ListView", WS_OVERLAPPEDWINDOW | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, static_cast(ceil(640.0f * dpiX / 96.0f)), static_cast(ceil(480.0f * dpiY / 96.0f)), NULL, NULL, HINST_THISCOMPONENT, this); hr = m_parentHwnd ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { ShowWindow(m_parentHwnd, SW_SHOWNORMAL); UpdateWindow(m_parentHwnd); } } // Create child window (for D2D content) { //register window class WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = ListViewApp::ChildWndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = sizeof(LONG_PTR); wcex.hInstance = HINST_THISCOMPONENT; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = NULL; wcex.lpszMenuName = NULL; wcex.lpszClassName = L"D2DListViewApp"; RegisterClassEx(&wcex); D2D1_SIZE_U d2dWindowSize = CalculateD2DWindowSize(); //create window m_d2dHwnd = CreateWindow( L"D2DListViewApp", L"", WS_CHILDWINDOW | WS_VISIBLE, 0, 0, d2dWindowSize.width, d2dWindowSize.height, m_parentHwnd, NULL, HINST_THISCOMPONENT, this ); hr = m_d2dHwnd ? S_OK : E_FAIL; } } return hr; } /****************************************************************** * * * ListViewApp::CalculateD2DWindowSize * * * * Determine the size of the D2D child window. * * * ******************************************************************/ D2D1_SIZE_U ListViewApp::CalculateD2DWindowSize() { RECT rc; GetClientRect(m_parentHwnd, &rc); D2D1_SIZE_U d2dWindowSize = {0}; d2dWindowSize.width = rc.right; d2dWindowSize.height = rc.bottom; return d2dWindowSize; } /****************************************************************** * * * ListViewApp::CreateDeviceIndependentResources * * * * This method is used to create resources which are not bound * * to any device. Their lifetime effectively extends for the * * duration of the app. These resources include the D2D, * * DWrite, and WIC factories; and a DWrite Text Format object * * (used for identifying particular font characteristics) and * * a D2D geometry. * * * ******************************************************************/ HRESULT ListViewApp::CreateDeviceIndependentResources() { static const WCHAR msc_fontName[] = L"Calibri"; static const FLOAT msc_fontSize = 20; HRESULT hr = S_OK; //create D2D factory hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); if (SUCCEEDED(hr)) { //create WIC factory hr = CoCreateInstance( CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, reinterpret_cast(&m_pWICFactory) ); } if (SUCCEEDED(hr)) { //create DWrite factory hr = DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(m_pDWriteFactory), reinterpret_cast(&m_pDWriteFactory) ); } if (SUCCEEDED(hr)) { //create DWrite text format object hr = m_pDWriteFactory->CreateTextFormat( msc_fontName, NULL, DWRITE_FONT_WEIGHT_THIN, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, msc_fontSize, L"", //locale &m_pTextFormat ); } if (SUCCEEDED(hr)) { //center the text both horizontally and vertically. m_pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); m_pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); IDWriteInlineObject *pEllipsis = NULL; hr = m_pDWriteFactory->CreateEllipsisTrimmingSign(m_pTextFormat, &pEllipsis); if (SUCCEEDED(hr)) { static const DWRITE_TRIMMING sc_trimming = { DWRITE_TRIMMING_GRANULARITY_CHARACTER, 0, 0 }; // Set the trimming back on the trimming format object. hr = m_pTextFormat->SetTrimming(&sc_trimming, pEllipsis); pEllipsis->Release(); } } if (SUCCEEDED(hr)) { // Set the text format not to allow word wrapping. hr = m_pTextFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP); } return hr; } /****************************************************************** * * * ListViewApp::CreateDeviceResources * * * * This method creates resources which are bound to a particular * * D3D device. It's all centralized here, in case the resources * * need to be recreated in case of D3D device loss (eg. display * * change, remoting, removal of video card, etc). * * * ******************************************************************/ HRESULT ListViewApp::CreateDeviceResources() { HRESULT hr = S_OK; if (!m_pRT) { RECT rc; GetClientRect(m_d2dHwnd, &rc); D2D1_SIZE_U size = D2D1::SizeU( rc.right - rc.left, rc.bottom - rc.top ); //create a D2D render target hr = m_pD2DFactory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(m_d2dHwnd, size), &m_pRT ); if (SUCCEEDED(hr)) { //create a black brush hr = m_pRT->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::Black), &m_pBlackBrush ); } if (SUCCEEDED(hr)) { hr = m_pRT->CreateBitmap( D2D1::SizeU(msc_atlasWidth, msc_atlasHeight), D2D1::BitmapProperties(D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED)), &m_pBitmapAtlas ); } if (SUCCEEDED(hr)) { hr = CreateBindCtx(0, &m_pBindContext); } if (SUCCEEDED(hr)) { hr = LoadDirectory(); } } return hr; } /****************************************************************** * * * ListViewApp::DiscardDeviceResources * * * * Discard device-specific resources which need to be recreated * * when a D3D device is lost * * * ******************************************************************/ void ListViewApp::DiscardDeviceResources() { SafeRelease(&m_pRT); SafeRelease(&m_pBitmapAtlas); SafeRelease(&m_pBlackBrush); } /****************************************************************** * * * ListViewApp::RunMessageLoop * * * * Main window message loop * * * ******************************************************************/ void ListViewApp::RunMessageLoop() { MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } } /****************************************************************** * * * ListViewApp::LoadDirectory * * * * Load item info from files in the current directory. * * Thumbnails/Icons are also loaded into the atlas and their * * location is stored within each ItemInfo object. * * * ******************************************************************/ HRESULT ListViewApp::LoadDirectory() { HRESULT hr = S_OK; // Locals that need to be cleaned up before we exit HDC memoryDC = CreateCompatibleDC(NULL); HBITMAP iconImage = NULL; WCHAR *wszAbsolutePath = NULL; BYTE *pBits = NULL; HANDLE directoryTraversalHandle = INVALID_HANDLE_VALUE; IShellItemImageFactory *pShellItemImageFactory = NULL; // Other locals static const UINT sc_bitsArraySize = msc_iconSize * msc_iconSize * 4; // 4 bytes per pixel in BGRA format. UINT absolutePathArraySize = 0; UINT currentX = 0; UINT currentY = 0; UINT i = 0; if (SUCCEEDED(hr)) { pBits = new (std::nothrow) BYTE[sc_bitsArraySize]; hr = pBits ? S_OK : E_OUTOFMEMORY; } if (SUCCEEDED(hr)) { // We have a static array of ItemInfo objects, so we can only load // msc_maxItemInfos ItemInfos. while (i < msc_maxItemInfos) { // We always load files from the current directory. We do navigation // by changing the current directory. The first time through the // while loop directoryTraversalHandle will be equal to // INVALID_HANDLE_VALUE and so we'll call FindFirstFile. On // subsequent interations we'll call FindNextFile to find other // items in the current directory. WIN32_FIND_DATA findFileData = {0}; if (directoryTraversalHandle == INVALID_HANDLE_VALUE) { directoryTraversalHandle = FindFirstFile(L".\\*", &findFileData); if (directoryTraversalHandle == INVALID_HANDLE_VALUE) { if (GetLastError() != ERROR_FILE_NOT_FOUND) { hr = E_FAIL; } break; } } else { if (!FindNextFile(directoryTraversalHandle, &findFileData)) { if (GetLastError() != ERROR_NO_MORE_FILES) { hr = E_FAIL; } break; } } m_pFiles[i].placement.left = currentX; m_pFiles[i].placement.top = currentY; // // Increment bitmap atlas position here so that we notice if we // don't have enough room for any more icons. // currentX += msc_iconSize; if (currentX + msc_iconSize > msc_atlasWidth) { currentX = 0; currentY += msc_iconSize; } if (currentY + msc_iconSize > msc_atlasHeight) { // Exceeded atlas size // We break without any error so that the contents up until this // point will be shown. break; } // // Determine the size of array needed to store the full path name. // We need the full path name to call SHCreateItemFromParsingName. // UINT requiredLength = GetFullPathName(findFileData.cFileName, 0, NULL, NULL); if (requiredLength == 0) { hr = E_FAIL; break; } // // Allocate a bigger buffer if necessary. // if (requiredLength > absolutePathArraySize) { delete[] wszAbsolutePath; wszAbsolutePath = new (std::nothrow) WCHAR[requiredLength]; if (wszAbsolutePath == NULL) { hr = E_FAIL; break; } absolutePathArraySize = requiredLength; } if (!GetFullPathName(findFileData.cFileName, requiredLength, wszAbsolutePath, NULL)) { hr = E_FAIL; break; } // Create an IShellItemImageFactory for the current directory item // so that we can get a icon/thumbnail for it. SafeRelease(&pShellItemImageFactory); hr = SHCreateItemFromParsingName( wszAbsolutePath, m_pBindContext, IID_PPV_ARGS(&pShellItemImageFactory) ); if (FAILED(hr)) { break; } SIZE iconSize; iconSize.cx = msc_iconSize; iconSize.cy = msc_iconSize; // If iconImage isn't NULL that means we're looping around. We call // DeleteObject to avoid leaking the HBITMAP. if (iconImage != NULL) { DeleteObject(iconImage); iconImage = NULL; } // Get the icon/thumbnail for the current directory item in HBITMAP // form. // In the interests of brevity, this sample calls GetImage from the // UI thread. However this function can be time consuming, so a real // application should call GetImage from a separate thread, showing // a placeholder icon until the icon has been loaded. hr = pShellItemImageFactory->GetImage(iconSize, 0x0, &iconImage); if (FAILED(hr)) { break; } BITMAPINFO bi = {0}; bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // Get the bitmap info header. if (0 == GetDIBits( memoryDC, // hdc iconImage, // hbmp 0, // uStartScan 0, // cScanLines NULL, // lpvBits &bi, DIB_RGB_COLORS )) { hr = E_FAIL; break; } // Positive bitmap info header height means bottom-up bitmaps. We // always use top-down bitmaps, so we set the height negative. if (bi.bmiHeader.biHeight > 0) { bi.bmiHeader.biHeight = -bi.bmiHeader.biHeight; } // If we happen to find an icon that's too big, skip over this item. if ( (-bi.bmiHeader.biHeight > msc_iconSize) || (bi.bmiHeader.biWidth > msc_iconSize) || (bi.bmiHeader.biSizeImage > sc_bitsArraySize)) { continue; } m_pFiles[i].isDirectory = (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; // Now that we know the size of the icon/thumbnail we can initialize // the rest of placement rectangle. We avoid using currentX/currentY // since we've already incremented those values in anticipation of // the next iteration of the loop. m_pFiles[i].placement.right = m_pFiles[i].placement.left + bi.bmiHeader.biWidth; m_pFiles[i].placement.bottom = m_pFiles[i].placement.top + -bi.bmiHeader.biHeight; // Now we copy the bitmap bits into a buffer. if (0 == GetDIBits( memoryDC, iconImage, 0, -bi.bmiHeader.biHeight, pBits, &bi, DIB_RGB_COLORS)) { hr = E_FAIL; break; } // Now we copy the buffer into video memory. hr = m_pBitmapAtlas->CopyFromMemory( &m_pFiles[i].placement, pBits, bi.bmiHeader.biSizeImage / -bi.bmiHeader.biHeight ); if (FAILED(hr)) { break; } StringCchCopy(m_pFiles[i].szFilename, MAX_PATH, findFileData.cFileName); // Set the previous position to 0 so that the items animate // downwards when they are first shown. m_pFiles[i].previousPosition = 0.0f; m_pFiles[i].currentPosition = static_cast(i * (msc_iconSize + msc_lineSpacing)); i++; } } if (SUCCEEDED(hr)) { m_numItemInfos = i; // // The total size of our document. // m_scrollRange = msc_iconSize * m_numItemInfos + msc_lineSpacing * (m_numItemInfos - 1); SCROLLINFO si = {0}; si.cbSize = sizeof(si); si.fMask = SIF_DISABLENOSCROLL | SIF_PAGE | SIF_POS | SIF_RANGE; si.nMin = 0; si.nMax = m_scrollRange; si.nPage = static_cast(m_pRT->GetSize().height); si.nPos = 0; SetScrollInfo(m_parentHwnd, SB_VERT, &si, TRUE); // // Animate the item positions into place. // m_animatingItems = msc_totalAnimatingItemFrames; // // Set the scroll to zero, don't animate. // m_animatingScroll = 0; m_currentScrollPos = 0; m_previousScrollPos = 0; } // // Clean up locals. // SafeRelease(&pShellItemImageFactory); delete[] pBits; delete[] wszAbsolutePath; if (directoryTraversalHandle != INVALID_HANDLE_VALUE) { FindClose(directoryTraversalHandle); } if (memoryDC != NULL) { DeleteDC(memoryDC); } if (iconImage != NULL) { DeleteObject(iconImage); } return hr; } /****************************************************************** * * * ListViewApp::OnRender * * * * Called whenever the application needs to display the client * * window. * * * * Note that this function will not render anything if the window * * is occluded (e.g. when the screen is locked). * * Also, this function will automatically discard device-specific * * resources if the D3D device disappears during function * * invocation, and will recreate the resources the next time it's * * invoked. * * * ******************************************************************/ HRESULT ListViewApp::OnRender() { HRESULT hr = S_OK; hr = CreateDeviceResources(); if (SUCCEEDED(hr) && !(m_pRT->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED)) { // We animate scrolling to achieve a smooth scrolling effect. // GetInterpolatedScrollPosition() returns the scroll position // for the current frame. FLOAT interpolatedScroll = GetInterpolatedScrollPosition(); m_pRT->BeginDraw(); // Displaying the correctly scrolled view is as simple as setting the // transform to translate by the current scroll amount. m_pRT->SetTransform(D2D1::Matrix3x2F::Translation(0, -interpolatedScroll)); m_pRT->Clear(D2D1::ColorF(D2D1::ColorF::White)); D2D1_SIZE_F rtSize = m_pRT->GetSize(); FLOAT interpolationFactor = GetAnimatingItemInterpolationFactor(); for (UINT i = 0; i < m_numItemInfos; i++) { Assert(m_pFiles[i].szFilename[0] != L'\0'); // We animate item position changes. The interpolation factor is the // a ratio between 0 and 1 used to interpolate between the previous // position and the current position. The position that we draw for // this frame is somewhere between the two. FLOAT interpolatedPosition = GetFancyAccelerationInterpolatedValue( interpolationFactor, m_pFiles[i].previousPosition, m_pFiles[i].currentPosition ); // We do a quick check to see if the items we are drawing will be in // the visible region. If they are not, we don't bother issues the // draw commands. This is a substantial perf win. FLOAT topOfIcon = interpolatedPosition; FLOAT bottomOfIcon = interpolatedPosition + msc_iconSize; if ( bottomOfIcon < interpolatedScroll || topOfIcon > interpolatedScroll + m_pRT->GetSize().height) { // Some further items could be in the visible region. Continue // the loop so that they will be drawn. continue; } // When the items change position we draw them mostly transparent // and then gradually make them more opaque as they get closer to // their final positions. This function was chosen after a bit of // experimentation and I thought it looked nice. FLOAT opacity = max(0.2f, interpolationFactor * interpolationFactor); // The icon is stored in the image atlas. We reference it's position // in the atlas and it's destination on the screen. m_pRT->DrawBitmap( m_pBitmapAtlas, D2D1::RectF( 0, interpolatedPosition, static_cast(msc_iconSize), interpolatedPosition + static_cast(msc_iconSize)), opacity, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, D2D1::RectF( (FLOAT)m_pFiles[i].placement.left, (FLOAT)m_pFiles[i].placement.top, (FLOAT)m_pFiles[i].placement.right, (FLOAT)m_pFiles[i].placement.bottom) ); // Draw the filename. For brevity we just use DrawText. A real // application should consider caching the TextLayout object during // animations to reduce CPU cost. m_pBlackBrush->SetOpacity(opacity); m_pRT->DrawText( m_pFiles[i].szFilename, static_cast(wcsnlen(m_pFiles[i].szFilename, ARRAYSIZE(m_pFiles[i].szFilename))), m_pTextFormat, D2D1::RectF( msc_iconSize + msc_lineSpacing, interpolatedPosition, rtSize.width, interpolatedPosition + static_cast(msc_iconSize)), m_pBlackBrush ); } hr = m_pRT->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { DiscardDeviceResources(); hr = S_OK; } } // Advance the position of the current item animation. if (m_animatingItems > 0) { --m_animatingItems; if (m_animatingItems == 0) { for (UINT i = 0; i < m_numItemInfos; i++) { m_pFiles[i].previousPosition = m_pFiles[i].currentPosition; } } InvalidateRect(m_d2dHwnd, NULL, FALSE); } // Advance the position of the current scroll animation if (m_animatingScroll > 0) { --m_animatingScroll; if (m_animatingScroll == 0) { m_previousScrollPos = m_currentScrollPos; } InvalidateRect(m_d2dHwnd, NULL, FALSE); } return hr; } /****************************************************************** * * * ListViewApp::GetFancyAccelerationInterpolatedValue * * * * Do a fancy interpolation between two points. * * * ******************************************************************/ FLOAT ListViewApp::GetFancyAccelerationInterpolatedValue(FLOAT linearFactor, FLOAT p1, FLOAT p2) { Assert(linearFactor >= 0.0f && linearFactor <= 1.0f); // Don't overshoot by more than the icon size. FLOAT apex = 0.0f; if (fabs(p1 - p2) > 0.01f) { apex = min(0.10f * fabs(p1 - p2), msc_iconSize) / fabs(p1 - p2); } else { apex = 0.0f; } // Stretch so that the initial overshoot (occurring 33% of the way along) is // 70% of the animation. FLOAT rearrangedDomain = 0.0f; if (linearFactor < 0.7f) { rearrangedDomain = (linearFactor / 0.7f) / 3.0f; } else { rearrangedDomain = ((linearFactor - 0.7f) / 0.3f) * (2.0f / 3.0f) + 1.0f/3.0f; } // // We will use sin to approximate the curve. Since we want to start at a // minimum value, we start at -PI/2. Since we want to finish at the second // max. We stretch the interval [0..1] to [-PI/2 .. 5PI/2]. // FLOAT stretchedDomain = rearrangedDomain * 3.0f * static_cast(M_PI); FLOAT translatedDomain = stretchedDomain - static_cast(M_PI_2); FLOAT fancyFactor = sin(translatedDomain) + 1.0f; // Now between 0 and 2 // // Before the first max, we want the bounds to go from 0 to 1+apex // if (translatedDomain < static_cast(M_PI_2)) { fancyFactor = fancyFactor * (1.0f+apex) / 2.0f; // Now between 0 and 1+apex } // // After the first max, we want to ease the bounds down so that when // translatedDomain is 5PI/2, fancyFactor is 1.0f. We also want the bounce // to be small, so we reduce the magnitude of the oscillation. // else { // // When we want the bounce (the undershoot after reaching the apex), to // be reach 1.0f - apex / 2.0f at a minimum. // FLOAT oscillationMin = (1.0f - apex / 2.0f); // // We want the max to start out at 1.0f + apex (so that we are // continuous) and finish at 1.0f (the final position). We square our // interpolation factor to stretch the bounce and compress the // correction. Since the correction is a smaller distance, this looks // better. Another benefit is that it prevents us from overshooting 1.0f // during the correction phase. // FLOAT interpolationFactor = (translatedDomain - static_cast(M_PI_2)) / (2.0f * static_cast(M_PI)); interpolationFactor *= interpolationFactor; FLOAT oscillationMax = 1.0f * interpolationFactor + (1.0f + apex) * (1.0f - interpolationFactor); Assert(oscillationMax >= oscillationMin); FLOAT oscillationMidPoint = (oscillationMin + oscillationMax) / 2.0f; FLOAT oscillationMagnitude = oscillationMax - oscillationMin; // Oscillate around the midpoint fancyFactor = (fancyFactor / 2.0f - 0.5f) * oscillationMagnitude + oscillationMidPoint; } return p2 * fancyFactor + p1 * (1.0f - fancyFactor); } /****************************************************************** * * * ListViewApp::GetAnimatingItemInterpolationFactor * * * * Return the interpolation factor for a linear interpolation * * for the current item animation for the the current frame * * * ******************************************************************/ FLOAT ListViewApp::GetAnimatingItemInterpolationFactor() { return static_cast(msc_totalAnimatingItemFrames - m_animatingItems) / msc_totalAnimatingItemFrames; } /****************************************************************** * * * ListViewApp::GetAnimatingScrollInterpolationFactor * * * * Return the interpolation factor for a linear interpolation * * for the current scroll animation for the the current frame * * * ******************************************************************/ FLOAT ListViewApp::GetAnimatingScrollInterpolationFactor() { return static_cast(msc_totalAnimatingScrollFrames - m_animatingScroll) / msc_totalAnimatingScrollFrames; } /****************************************************************** * * * ListViewApp::GetInterpolatedScrollPosition * * * * Return the scroll position for the current frame * * * ******************************************************************/ FLOAT ListViewApp::GetInterpolatedScrollPosition() { FLOAT interpolationFactor = GetAnimatingScrollInterpolationFactor(); return m_currentScrollPos * interpolationFactor + m_previousScrollPos * (1.0f - interpolationFactor); } /****************************************************************** * * * ListViewApp::OnResize * * * * If the application receives a WM_SIZE message, this method * * resizes the render target appropriately. * * * ******************************************************************/ void ListViewApp::OnResize() { if (m_pRT) { D2D1_SIZE_U size = CalculateD2DWindowSize(); MoveWindow(m_d2dHwnd, 0, 0, size.width, size.height, FALSE); // Note: This method can fail, but it's okay to ignore the // error here -- it will be repeated on the next call to // EndDraw. m_pRT->Resize(size); m_scrollRange = (msc_lineSpacing + msc_iconSize) * m_numItemInfos - msc_lineSpacing; SCROLLINFO si = {0}; si.cbSize = sizeof(si); si.fMask = SIF_DISABLENOSCROLL | SIF_PAGE | SIF_RANGE; si.nMin = 0; si.nMax = m_scrollRange; si.nPage = size.height; SetScrollInfo(m_parentHwnd, SB_VERT, &si, TRUE); InvalidateRect(m_d2dHwnd, NULL, FALSE); } } /****************************************************************** * * * ListViewApp::CompareAToZ (static) * * * * A comparator function for sorting ItemInfos alphabetically * * * ******************************************************************/ int ListViewApp::CompareAToZ(const void *a, const void *b) { const ListViewApp::ItemInfo *pA = reinterpret_cast(a); const ListViewApp::ItemInfo *pB = reinterpret_cast(b); return wcscmp(pA->szFilename, pB->szFilename); } /****************************************************************** * * * ListViewApp::CompareZToA (static) * * * * A comparator function for sorting ItemInfos in reverse * * alphabetical order. * * * ******************************************************************/ int ListViewApp::CompareZToA(const void *a, const void *b) { const ListViewApp::ItemInfo *pA = reinterpret_cast(a); const ListViewApp::ItemInfo *pB = reinterpret_cast(b); return wcscmp(pB->szFilename, pA->szFilename); } /****************************************************************** * * * ListViewApp::CompareDirFirstAToZ (static) * * * * A comparator function for sorting ItemInfos in alphabetical * * order, with all directories before all other files. * * * ******************************************************************/ int ListViewApp::CompareDirFirstAToZ(const void *a, const void *b) { const ListViewApp::ItemInfo *pA = reinterpret_cast(a); const ListViewApp::ItemInfo *pB = reinterpret_cast(b); if (pA->isDirectory && !pB->isDirectory) { return -1; } else if (!pA->isDirectory && pB->isDirectory) { return 1; } else { return wcscmp(pA->szFilename, pB->szFilename); } } /****************************************************************** * * * ListViewApp::OnChar * * * * Called when the app receives a WM_CHAR message (which happens * * when a key is pressed). * * * ******************************************************************/ void ListViewApp::OnChar(SHORT aChar) { // We only do stuff for 'a', 'z', or 'd' if (aChar == 'a' || aChar == 'z' || aChar == 'd') { typedef int (__cdecl * Comparator) (const void *, const void *); Comparator comparator = NULL; // 'a' means alphabetical sort if (aChar == 'a') { comparator = ListViewApp::CompareAToZ; } // 'z' means reverse alphabetical sort else if (aChar == 'z') { comparator = ListViewApp::CompareZToA; } // 'd' means alphabetical sort, directories first else { comparator = ListViewApp::CompareDirFirstAToZ; } // Freeze file position to the current interpolated position so that // when we animate to the new positions, the items don't jump back to // their previous position momentarily. for (UINT i = 0; i < m_numItemInfos; i++) { FLOAT interpolationFactor = GetAnimatingItemInterpolationFactor(); m_pFiles[i].previousPosition = GetFancyAccelerationInterpolatedValue( interpolationFactor, m_pFiles[i].previousPosition, m_pFiles[i].currentPosition ); } // Apply the new sort. qsort(m_pFiles, m_numItemInfos, sizeof(ListViewApp::ItemInfo), comparator); // Set the new positions based up on the position of each item within // the sorted array. for (UINT i = 0; i < m_numItemInfos; i++) { m_pFiles[i].currentPosition = static_cast(i * (msc_iconSize + msc_lineSpacing)); } // Animate the items to their new positions. m_animatingItems = msc_totalAnimatingItemFrames; InvalidateRect(m_d2dHwnd, NULL, FALSE); } } /****************************************************************** * * * ListViewApp::GetScrolledDIPositionFromPixelPosition * * * * Translate a pixel position to a position within our document, * * taking scrolling into account. * * * ******************************************************************/ D2D1_POINT_2F ListViewApp::GetScrolledDIPositionFromPixelPosition(D2D1_POINT_2U pixelPosition) { D2D1_POINT_2F dpi = {0}; m_pRT->GetDpi(&dpi.x, &dpi.y); return D2D1::Point2F(pixelPosition.x * 96 / dpi.x, pixelPosition.y * 96 / dpi.y + GetInterpolatedScrollPosition()); } /****************************************************************** * * * ListViewApp::ParentWndProc * * * * Window message handler * * * ******************************************************************/ LRESULT CALLBACK ListViewApp::ParentWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT result = 0; if (message == WM_CREATE) { LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam; ListViewApp *pListViewApp = (ListViewApp *)pcs->lpCreateParams; ::SetWindowLongPtrW( hwnd, GWLP_USERDATA, reinterpret_cast(pListViewApp) ); result = 1; } else { ListViewApp *pListViewApp = reinterpret_cast( ::GetWindowLongPtrW( hwnd, GWLP_USERDATA )); bool wasHandled = false; if (pListViewApp) { switch (message) { case WM_SIZE: { pListViewApp->OnResize(); } result = 0; wasHandled = true; break; case WM_VSCROLL: { pListViewApp->OnVScroll(wParam, lParam); } result = 0; wasHandled = true; break; case WM_MOUSEWHEEL: { pListViewApp->OnMouseWheel(wParam, lParam); } result = 0; wasHandled = true; break; case WM_CHAR: { pListViewApp->OnChar(static_cast(wParam)); } result = 0; wasHandled = true; break; case WM_DESTROY: { PostQuitMessage(0); } result = 1; wasHandled = true; break; } } if (!wasHandled) { result = DefWindowProc(hwnd, message, wParam, lParam); } } return result; } /****************************************************************** * * * ListViewApp::ChildWndProc * * * * Window message handler for the Child D2D window * * * ******************************************************************/ LRESULT CALLBACK ListViewApp::ChildWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_CREATE) { LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam; ListViewApp *pListViewApp = (ListViewApp *)pcs->lpCreateParams; ::SetWindowLongPtrW( hwnd, GWLP_USERDATA, PtrToUlong(pListViewApp) ); return 1; } ListViewApp *pListViewApp = reinterpret_cast(static_cast( ::GetWindowLongPtrW( hwnd, GWLP_USERDATA ))); if (pListViewApp) { switch (message) { case WM_PAINT: case WM_DISPLAYCHANGE: { PAINTSTRUCT ps; BeginPaint(hwnd, &ps); pListViewApp->OnRender(); EndPaint(hwnd, &ps); } return 0; case WM_LBUTTONDOWN: { D2D1_POINT_2F diPosition = pListViewApp->GetScrolledDIPositionFromPixelPosition(D2D1::Point2U(LOWORD(lParam), HIWORD(lParam))); pListViewApp->OnLeftButtonDown(diPosition); } return 0; case WM_DESTROY: { PostQuitMessage(0); } return 1; } } return DefWindowProc(hwnd, message, wParam, lParam); } /****************************************************************** * * * ListViewApp::OnLeftButtonDown * * * * Called when the left mouse button is pressed inside the child * * D2D window * * * ******************************************************************/ void ListViewApp::OnLeftButtonDown(D2D1_POINT_2F diPosition) { INT index = static_cast(diPosition.y / (msc_iconSize + msc_lineSpacing)); if (index >= 0 && index < static_cast(m_numItemInfos)) { // Only process the click if the item isn't animating if (m_pFiles[index].currentPosition == m_pFiles[index].previousPosition) { if (diPosition.y < m_pFiles[index].currentPosition + msc_iconSize) { if (SetCurrentDirectory(m_pFiles[index].szFilename)) { if (SUCCEEDED(LoadDirectory())) { InvalidateRect(m_d2dHwnd, NULL, FALSE); } } } } } } /****************************************************************** * * * ListViewApp::OnLeftButtonDown * * * * Called when the mouse wheel is moved. * * * ******************************************************************/ void ListViewApp::OnMouseWheel(WPARAM wParam, LPARAM /* lParam */) { m_previousScrollPos = static_cast(GetInterpolatedScrollPosition()); m_currentScrollPos -= GET_WHEEL_DELTA_WPARAM(wParam); m_currentScrollPos = max(0, min(m_currentScrollPos, static_cast(m_scrollRange) - static_cast(m_pRT->GetSize().height))); SCROLLINFO si = {0}; si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE | SIF_TRACKPOS; BOOL ret = GetScrollInfo(m_parentHwnd, SB_VERT, &si); if (!ret) { Assert(ret); return; } if (m_currentScrollPos != si.nPos) { si.nPos = m_currentScrollPos; SetScrollInfo(m_parentHwnd, SB_VERT, &si, TRUE); m_animatingScroll = msc_totalAnimatingScrollFrames; InvalidateRect(m_d2dHwnd, NULL, FALSE); } } /****************************************************************** * * * ListViewApp::OnVScroll * * * * Called when a WM_VSCROLL message is sent. * * * ******************************************************************/ void ListViewApp::OnVScroll(WPARAM wParam, LPARAM /* lParam */) { INT newScrollPos = m_currentScrollPos; switch (LOWORD(wParam)) { case SB_LINEUP: newScrollPos -= 1; break; case SB_LINEDOWN: newScrollPos += 1; break; case SB_PAGEUP: newScrollPos -= static_cast(m_pRT->GetSize().height); break; case SB_PAGEDOWN: newScrollPos += static_cast(m_pRT->GetSize().height); break; case SB_THUMBTRACK: { SCROLLINFO si = {0}; si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE | SIF_TRACKPOS; BOOL ret = GetScrollInfo(m_parentHwnd, SB_VERT, &si); if (!ret) { Assert(ret); return; } newScrollPos = si.nTrackPos; } break; default: break; } newScrollPos = max(0, min(newScrollPos, static_cast(m_scrollRange))); m_previousScrollPos = static_cast(GetInterpolatedScrollPosition()); m_currentScrollPos = newScrollPos; SCROLLINFO si = {0}; si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE | SIF_TRACKPOS; BOOL ret = GetScrollInfo(m_parentHwnd, SB_VERT, &si); if (!ret) { Assert(ret); return; } if (m_currentScrollPos != si.nPos) { si.nPos = m_currentScrollPos; SetScrollInfo(m_parentHwnd, SB_VERT, &si, TRUE); m_animatingScroll = msc_totalAnimatingScrollFrames; InvalidateRect(m_d2dHwnd, NULL, FALSE); } }