2025-11-28 00:35:46 +09:00

1475 lines
50 KiB
C++

// 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<UINT>(ceil(640.0f * dpiX / 96.0f)),
static_cast<UINT>(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<void **>(&m_pWICFactory)
);
}
if (SUCCEEDED(hr))
{
//create DWrite factory
hr = DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(m_pDWriteFactory),
reinterpret_cast<IUnknown **>(&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<FLOAT>(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<UINT>(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<FLOAT>(msc_iconSize),
interpolatedPosition + static_cast<FLOAT>(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<UINT>(wcsnlen(m_pFiles[i].szFilename, ARRAYSIZE(m_pFiles[i].szFilename))),
m_pTextFormat,
D2D1::RectF(
msc_iconSize + msc_lineSpacing,
interpolatedPosition,
rtSize.width,
interpolatedPosition + static_cast<FLOAT>(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<FLOAT>(M_PI);
FLOAT translatedDomain = stretchedDomain - static_cast<FLOAT>(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<FLOAT>(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<FLOAT>(M_PI_2)) / (2.0f * static_cast<FLOAT>(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<FLOAT>(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<FLOAT>(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<const ListViewApp::ItemInfo *>(a);
const ListViewApp::ItemInfo *pB = reinterpret_cast<const ListViewApp::ItemInfo *>(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<const ListViewApp::ItemInfo *>(a);
const ListViewApp::ItemInfo *pB = reinterpret_cast<const ListViewApp::ItemInfo *>(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<const ListViewApp::ItemInfo *>(a);
const ListViewApp::ItemInfo *pB = reinterpret_cast<const ListViewApp::ItemInfo *>(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<FLOAT>(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<LONG_PTR>(pListViewApp)
);
result = 1;
}
else
{
ListViewApp *pListViewApp = reinterpret_cast<ListViewApp *>(
::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<SHORT>(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<ListViewApp *>(static_cast<LONG_PTR>(
::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<INT>(diPosition.y / (msc_iconSize + msc_lineSpacing));
if (index >= 0 && index < static_cast<INT>(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<INT>(GetInterpolatedScrollPosition());
m_currentScrollPos -= GET_WHEEL_DELTA_WPARAM(wParam);
m_currentScrollPos = max(0, min(m_currentScrollPos, static_cast<INT>(m_scrollRange) - static_cast<INT>(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<UINT>(m_pRT->GetSize().height);
break;
case SB_PAGEDOWN:
newScrollPos += static_cast<UINT>(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<INT>(m_scrollRange)));
m_previousScrollPos = static_cast<INT>(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);
}
}