Click here to Skip to main content
15,896,154 members
Articles / Desktop Programming / MFC

MFC Snapping Windows

Rate me:
Please Sign up or sign in to vote.
4.68/5 (17 votes)
27 May 2014GPL34 min read 44.9K   2.7K   56  
Easily create windows that snap to each other.
/*
 * CExtWS.h
 *
 * author j.wolfe <vachaun22@gmail.com>
 * copyright (c) 2012 all rights reserved
 *
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *
 * Portions of code handling the CMap objects and CList objects based on
 * code from Paolo Massina's child anchoring classes which were adapted into
 * the Prof-UIS suite as a CExtWA template.
 *
 * Portions of code positioning windows based on code found in the 
 * CWinampWnd class found on the CodeProject site.
 *
 */

#ifndef _CEXTWS_H_
#define _CEXTWS_H_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000

// Messages that get passed between parent and child windows
#define WM_PARENT_MOVING		WM_USER+0x100
#define WM_DOCK_STATUS			WM_USER+0x101
#define WM_REMOVE_SNAPPER		WM_USER+0x102
#define WM_SET_PARENTHWND		WM_USER+0x103
#define WM_PARENT_MOVED			WM_USER+0x104

// Message notification can be handle outside of the class
#define WM_NOTIFY_DOCK			WM_USER+0x105

#define SNAP_OFFSET			10

#define AERO_OFFSET			 5

#define DOCK_NONE			 0
#define DOCK_TOP			 1
#define DOCK_LEFT			 2
#define DOCK_RIGHT			 4
#define DOCK_BOTTOM			 8

typedef HRESULT (WINAPI *DwmIsCompositionEnabledFunc)(_Out_ BOOL *pfEnabled);
typedef HRESULT (WINAPI *DwmGetWindowAttributeFunc)(
	HWND hwnd,
	DWORD dwAttribute,
	_Out_	PVOID pvAttribute,
	DWORD	cbAttribute
);

#ifndef _DWMAPI_H_
// Window attributes
enum DWMWINDOWATTRIBUTE
{
    DWMWA_NCRENDERING_ENABLED = 1,      // [get] Is non-client rendering enabled/disabled
    DWMWA_NCRENDERING_POLICY,           // [set] Non-client rendering policy
    DWMWA_TRANSITIONS_FORCEDISABLED,    // [set] Potentially enable/forcibly disable transitions
    DWMWA_ALLOW_NCPAINT,                // [set] Allow contents rendered in the non-client area to be visible on the DWM-drawn frame.
    DWMWA_CAPTION_BUTTON_BOUNDS,        // [get] Bounds of the caption button area in window-relative space.
    DWMWA_NONCLIENT_RTL_LAYOUT,         // [set] Is non-client content RTL mirrored
    DWMWA_FORCE_ICONIC_REPRESENTATION,  // [set] Force this window to display iconic thumbnails.
    DWMWA_FLIP3D_POLICY,                // [set] Designates how Flip3D will treat the window.
    DWMWA_EXTENDED_FRAME_BOUNDS,        // [get] Gets the extended frame bounds rectangle in screen space
    DWMWA_HAS_ICONIC_BITMAP,            // [set] Indicates an available bitmap when there is no better thumbnail representation.
    DWMWA_DISALLOW_PEEK,                // [set] Don't invoke Peek on the window.
    DWMWA_EXCLUDED_FROM_PEEK,           // [set] LivePreview exclusion information
    DWMWA_LAST
};
#endif

/*
 * CExtPS
 * 
 * The parent window that other windows will snap to
 */

template < class CExtPSBase >
class CExtPS : public CExtPSBase
{
protected:
	struct SDI_t
	{
		BOOL	m_bIsDocked;
		UINT	m_nDockSide;
		int		m_nSnapOffset;
		SDI_t(BOOL bIsDocked = FALSE,
			  UINT nDockSide = DOCK_NONE, 
			  int nSnapOffset = SNAP_OFFSET)
		: m_bIsDocked(bIsDocked)
		, m_nDockSide(nDockSide)
		, m_nSnapOffset(nSnapOffset)
		{ }
		SDI_t(const SDI_t &other)
		: m_bIsDocked(other.m_bIsDocked)
		, m_nDockSide(other.m_nDockSide)
		, m_nSnapOffset(other.m_nSnapOffset)
		{ }
		SDI_t & operator= (const SDI_t &other)
		{
			m_bIsDocked = other.m_bIsDocked;
			m_nDockSide = other.m_nDockSide;
			m_nSnapOffset = other.m_nSnapOffset;
			return *this;
		}
	}; // struct SDI_t
	// map of snapping windows
	typedef CMap < HWND, HWND, SDI_t, SDI_t > CMapSDI;
	CMapSDI m_mapSDI;
public:
	CExtPS()
		{ }
	CExtPS(UINT nIDTemplate)
		: CExtPSBase(nIDTemplate)
		{ }
	CExtPS(UINT nIDTemplate, CWnd * pParentWnd)
		: CExtPSBase(nIDTemplate, pParentWnd)
		{ }
	CExtPS(LPCTSTR lpszTemplateName, CWnd * pParentWnd)
		: CExtPSBase(lpszTemplateName, pParentWnd)
		{ }
	CExtPS(UINT nIDTemplate, UINT nIDCaption/* = 0*/)
		: CExtPSBase(nIDTemplate, nIDCaption)
		{ }
	CExtPS(LPCTSTR lpszTemplateName, UINT nIDCaption = 0)
		: CExtPSBase(lpszTemplateName, nIDCaption)
		{ }
	CExtPS(UINT nIDCaption, CWnd * pParentWnd, UINT iSelectPage)
		: CExtPSBase(nIDCaption, pParentWnd, iSelectPage)
		{ }
	CExtPS(LPCTSTR lpszCaption, CWnd * pParentWnd, UINT iSelectPage)
		: CExtPSBase(lpszCaption, pParentWnd, iSelectPage)
		{ }
	virtual ~CExtPS()
		{ RemoveAllSnappers(); }
	bool RemoveSnapper(HWND hwnd)
	{
		if (hwnd == NULL)
			return false;
		return m_mapSDI.RemoveKey(hwnd) ? true : false;
	}
	void RemoveAllSnappers()
	{
		m_mapSDI.RemoveAll();
	}
	bool AddSnapper(CWnd *pWindow)
	{
		HWND hwnd = pWindow->GetSafeHwnd();
		if ((hwnd == NULL) || !::IsWindow(hwnd))
			return false;
		return AddSnapper(hwnd);
	}
	virtual bool AddSnapper(HWND hwnd)
	{
		if ((hwnd == NULL) || !::IsWindow(hwnd))
			return false;
		m_mapSDI.SetAt(hwnd, SDI_t());
		::SendMessage(hwnd, WM_SET_PARENTHWND, (WPARAM)GetSafeHwnd(), 0L);
		return true;
	}
protected:
	virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
	{
		if ((message == WM_MOVE) ||
			(message == WM_SIZE))	/* WM_MOVING|WM_SIZING not sent when not
									 * showing window contents while dragging */
		{
			LRESULT lResult = CExtPSBase::WindowProc(message, wParam, lParam);
			CRect rcWindow;
			GetWindowRect(&rcWindow);
			MoveSnappers(wParam, (LPARAM)&rcWindow);
			NotifyUnsnapped();
			return lResult;
		} // WM_MOVE || WM_SIZE 
		if ((message == WM_MOVING) ||
			(message == WM_SIZING))
		{
			LRESULT lResult = CExtPSBase::WindowProc(message, wParam, lParam);
			MoveSnappers(wParam, lParam);
			return lResult;
		} // WM_MOVING || WM_SIZING
		if (message == WM_DOCK_STATUS)
		{
			HWND _hwnd = (HWND)wParam;
			BOOL _bIsDocked = (BOOL)LOWORD(lParam);
			UINT _nDockSide = (UINT)HIWORD(lParam);
			UpdateDockStatus(_hwnd, _bIsDocked, _nDockSide);
			return 0;
		} // WM_DOCK_STATUS
		if (message == WM_REMOVE_SNAPPER)
		{
			HWND _hwnd = (HWND)wParam;
			RemoveSnapper(_hwnd);
			return 0;
		} // WM_REMOVE_SNAPPER
		CWnd * pWndThis = this;
		HWND hWndOwn = m_hWnd;
		LRESULT lResult = CExtPSBase::WindowProc(message, wParam, lParam);
		if (message == WM_DESTROY ||
			message == WM_NCDESTROY)
		{
			if (hWndOwn != NULL &&
				::IsWindow(hWndOwn) &&
				CWnd::FromHandlePermanent(hWndOwn) == pWndThis)
			{
				RemoveAllSnappers();
			}
		}
		return lResult;
	}
private:
	void NotifyUnsnapped()
	{
		INT nCount = INT(m_mapSDI.GetCount());
		if (nCount <= 0)
			return;
		CList < HWND, HWND > listInvalidHWNDs;
		POSITION pos = m_mapSDI.GetStartPosition();
		for( ; pos != NULL; )
		{
			HWND _hwnd;
			SDI_t _sdi;
			m_mapSDI.GetNextAssoc(pos, _hwnd, _sdi);
			if (!::IsWindow(_hwnd))
			{
				listInvalidHWNDs.AddTail(_hwnd);
				continue;
			}
			if (!_sdi.m_bIsDocked)
				::PostMessage(_hwnd, WM_PARENT_MOVED, 0L, 0L);
		}
		if (listInvalidHWNDs.GetCount() > 0)
		{
			pos = listInvalidHWNDs.GetHeadPosition();
			for( ; pos != NULL; )
			{
				HWND _hwndInvalid = listInvalidHWNDs.GetNext(pos);
				m_mapSDI.RemoveKey(_hwndInvalid);
			}
		}
	}
	void MoveSnappers(WPARAM wParam, LPARAM lParam)
	{
		INT nCount = INT(m_mapSDI.GetCount());
		if (nCount <= 0)
			return;
		CList < HWND, HWND > listInvalidHWNDs;
		POSITION pos = m_mapSDI.GetStartPosition();
		for( ; pos != NULL; )
		{
			HWND _hwnd;
			SDI_t _sdi;
			m_mapSDI.GetNextAssoc(pos, _hwnd, _sdi);
			if (!::IsWindow(_hwnd))
			{
				listInvalidHWNDs.AddTail(_hwnd);
				continue;
			}
			if (_sdi.m_bIsDocked)
				::PostMessage(_hwnd, WM_PARENT_MOVING, (WPARAM)_sdi.m_nDockSide, lParam);
		}
		if (listInvalidHWNDs.GetCount() > 0)
		{
			pos = listInvalidHWNDs.GetHeadPosition();
			for( ; pos != NULL; )
			{
				HWND _hwndInvalid = listInvalidHWNDs.GetNext(pos);
				m_mapSDI.RemoveKey(_hwndInvalid);
			}
		}
	}
	void UpdateDockStatus(HWND _hwnd, BOOL _bIsDocked, UINT _nDockSide)
	{
		if (!::IsWindow(_hwnd))
			return;
    
		CMapSDI::CPair *pCurVal;
    
		pCurVal = m_mapSDI.PLookup(_hwnd);
		pCurVal->value.m_bIsDocked = _bIsDocked;
		pCurVal->value.m_nDockSide = _nDockSide;
	}
}; // class CExtPS : public CExtPSBase

/*
 * CExtCS
 * 
 * The child window that will snap to other windows
 */

template < class CExtCSBase >
class CExtCS : public CExtCSBase
{
protected:
	HWND	m_hwndParent;
	BOOL	m_bIsDocked;
	int		m_nSnapOffset;
	int		m_nAeroOffset;
	UINT	m_nDockSide;
	BOOL	m_bAeroInUse;

public:
	CExtCS()
		{ Initialize(); }
	CExtCS(UINT nIDTemplate, CWnd * pParentWnd)
		: CExtCSBase(nIDTemplate, pParentWnd)
		{ Initialize(); }
	CExtCS(LPCTSTR lpszTemplateName, CWnd * pParentWnd)
		: CExtCSBase(lpszTemplateName, pParentWnd)
		{ Initialize(); }
	CExtCS(UINT nIDTemplate, UINT nIDCaption = 0)
		: CExtCSBase(nIDTemplate, nIDCaption)
		{ Initialize(); }
	CExtCS(LPCTSTR lpszTemplateName, UINT nIDCaption = 0)
		: CExtCSBase(lpszTemplateName, nIDCaption)
		{ Initialize(); }
	CExtCS(UINT nIDCaption, CWnd * pParentWnd, UINT iSelectPage)
		: CExtCSBase(nIDCaption, pParentWnd, iSelectPage)
		{ Initialize(); }
	CExtCS(LPCTSTR lpszCaption, CWnd * pParentWnd, UINT iSelectPage)
		: CExtCSBase(lpszCaption, pParentWnd, iSelectPage)
		{ Initialize(); }
	virtual ~CExtCS()
		{ }
	virtual void OnOK()
	{
		RemoveSnapper();

		CExtCSBase::OnOK();
	}
	virtual void OnCancel()
	{
		RemoveSnapper();

		CExtCSBase::OnCancel();
	}
	void SetSnapOffset(int nSnapOffset = SNAP_OFFSET)
	  { m_nSnapOffset = nSnapOffset; }
	void DockNow(UINT nDockSide = DOCK_RIGHT)
	{
		m_bIsDocked = TRUE;
		m_nDockSide = nDockSide;
		if ((m_hwndParent != NULL) &&
			::IsWindow(m_hwndParent))
		{
			CRect rcParent;
			::GetWindowRect(m_hwndParent, &rcParent);
			UpdateWindowPos(m_nDockSide, &rcParent, TRUE);
			::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(), MAKELPARAM((WORD)m_bIsDocked, (WORD)m_nDockSide));
		}
	}
protected:
	virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
	{
		if (message == WM_MOVE)
		{
			LRESULT lResult = CExtCSBase::WindowProc(message, wParam, lParam);
			Dock(TRUE);
			return lResult;
		}	// WM_MOVE
		if (message == WM_EXITSIZEMOVE)
		{
			if (m_bIsDocked)
			{
				CRect rcParent;
				::GetWindowRect(m_hwndParent, &rcParent);
				UpdateWindowPos(m_nDockSide, &rcParent, TRUE);
			}
			return CExtCSBase::WindowProc(message, wParam, lParam);
		}	// WM_LBUTTONUP
		if (message == WM_PARENT_MOVED)
		{
			Dock(FALSE);
			return 0L;
		}	// WM_PARENT_MOVED
		if (message == WM_PARENT_MOVING)
		{
			UINT _nDockSide = UINT(wParam);
			CRect rcParent;
			::GetWindowRect(m_hwndParent, &rcParent);
			UpdateWindowPos(_nDockSide, &rcParent, FALSE);
			return 0L;
		}	// WM_PARENT_MOVING
		if (message == WM_SET_PARENTHWND)
		{
			HWND _hwnd = HWND(wParam);
			if (_hwnd != NULL &&
			::IsWindow(_hwnd))
			{
				m_hwndParent = _hwnd;
			}
		}	// WM_SET_PARENTHWND
		if (message == WM_SETTINGCHANGE)
		{
			TRACE("WM_SETTINGCHANGE\n");
			UpdateOffsets();
		}	// WM_SETTINGCHANGE
		LRESULT lResult = CExtCSBase::WindowProc(message, wParam, lParam);
		if (message == WM_DESTROY ||
			message == WM_NCDESTROY)
		{
			RemoveSnapper();
		}
		return lResult;
	}
private:
	void Initialize()
	{
		m_hwndParent = NULL;
		m_bIsDocked = FALSE;
		m_nDockSide = DOCK_NONE;
		UpdateOffsets();
	}
	void UpdateOffsets()
	{
		m_nAeroOffset = AERO_OFFSET;
		m_bAeroInUse = IsAeroEnabled();
		m_nAeroOffset = (m_bAeroInUse) ? GetAeroOffset() : AERO_OFFSET;
		m_nSnapOffset = (m_bAeroInUse) ? (SNAP_OFFSET + m_nAeroOffset) : SNAP_OFFSET;
	}
	void RemoveSnapper()
	{
		CWnd * pWndThis = this;
		HWND hWndOwn = m_hWnd;
		if (hWndOwn != NULL &&
			::IsWindow(hWndOwn) &&
			CWnd::FromHandlePermanent(hWndOwn) == pWndThis)
		{
			::PostMessageA(m_hwndParent, WM_REMOVE_SNAPPER, (WPARAM)hWndOwn, 0L);
		}
	}
	void UpdateWindowPos(UINT _nDockSide, LPRECT lprcParent, BOOL bActivate)
	{
		CRect rcWindow;
		GetWindowRect(&rcWindow);
		UINT nFlags = (bActivate) ? SWP_SHOWWINDOW : SWP_SHOWWINDOW | SWP_NOACTIVATE;

#if (_MFC_PLATFORM_TOOLSET < 100)
		if (m_bAeroInUse)
		{
			if (_nDockSide == DOCK_LEFT)
				::OffsetRect(lprcParent, (m_nAeroOffset * (-1)), m_nAeroOffset);
			else if (_nDockSide == DOCK_RIGHT || _nDockSide == DOCK_BOTTOM)
				::OffsetRect(lprcParent, m_nAeroOffset, m_nAeroOffset);
			else if (_nDockSide == DOCK_TOP)
				::OffsetRect(lprcParent, m_nAeroOffset, (m_nAeroOffset * (-1)));
		}
#endif

		if (_nDockSide == DOCK_LEFT)
		{
			SetWindowPos(NULL, (lprcParent->left - rcWindow.Width()), lprcParent->top, rcWindow.Width(), rcWindow.Height(), nFlags);
		}	// DOCK_LEFT
		else if (_nDockSide == DOCK_RIGHT)
		{
			SetWindowPos(NULL, lprcParent->right, lprcParent->top, rcWindow.Width(), rcWindow.Height(), nFlags);
		}	// DOCK_RIGHT
		else if (_nDockSide == DOCK_TOP)
		{
			SetWindowPos(NULL, lprcParent->left, (lprcParent->top - rcWindow.Height()), rcWindow.Width(), rcWindow.Height(), nFlags);
		}	// DOCK_TOP
		else if (_nDockSide == DOCK_BOTTOM)
		{
			SetWindowPos(NULL, lprcParent->left, lprcParent->bottom, rcWindow.Width(), rcWindow.Height(), nFlags);
		}	// DOCK_BOTTOM
	}
	void Dock(BOOL bActivate)
	{
		CRect rcParent, rcWindow;
		if (m_hwndParent == NULL &&
			!::IsWindow(m_hwndParent))
			return;
    
		::GetWindowRect(m_hwndParent, &rcParent);
		GetWindowRect(&rcWindow);
    
		// Dock to right side of window
		if ((rcWindow.left <= (rcParent.right + m_nSnapOffset)) &&
			(rcWindow.left >= (rcParent.right - m_nSnapOffset)))
		{
			if (!m_bIsDocked)
			{
				m_bIsDocked = TRUE;
				m_nDockSide = DOCK_RIGHT;
				::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(), MAKELPARAM((WORD)m_bIsDocked, (WORD)DOCK_RIGHT));
			}
		}	// Dock to right side of window
		// Dock to left side of window
		else if ((rcWindow.right <= (rcParent.left + m_nSnapOffset)) &&
				 (rcWindow.right >= (rcParent.left - m_nSnapOffset)))
		{
			if (!m_bIsDocked)
			{
				m_bIsDocked = TRUE;
				m_nDockSide = DOCK_LEFT;
				::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(), MAKELPARAM((WORD)m_bIsDocked, (WORD)DOCK_LEFT));
			}
		}	// Dock to left side of window
		// Dock to top of window
		else if ((rcWindow.bottom <= (rcParent.top + m_nSnapOffset)) &&
				 (rcWindow.bottom >= (rcParent.top - m_nSnapOffset)))
		{
			if (!m_bIsDocked)
			{
				m_bIsDocked = TRUE;
				m_nDockSide = DOCK_TOP;
				::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(), MAKELPARAM((WORD)m_bIsDocked, (WORD)DOCK_TOP));
			}
		}	// Dock to top side of window
		// Dock to bottom side of window
		else if ((rcWindow.top <= (rcParent.bottom + m_nSnapOffset)) &&
				 (rcWindow.top >= (rcParent.bottom - m_nSnapOffset)))
		{
			if (!m_bIsDocked)
			{
				m_bIsDocked = TRUE;
				m_nDockSide = DOCK_BOTTOM;
				::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(), MAKELPARAM((WORD)m_bIsDocked, (WORD)DOCK_BOTTOM));
			}
		}	// Dock to bottom side of window
		// Window not docked
		else
		{
			if (m_bIsDocked)
			{
				m_bIsDocked = FALSE;
				m_nDockSide = DOCK_NONE;
				::PostMessage(m_hwndParent, WM_DOCK_STATUS, (WPARAM)GetSafeHwnd(),MAKELPARAM((WORD)m_bIsDocked, (WORD)DOCK_NONE));
			}
		}
		
		// Post a notification message to self to be used if needed
		PostMessage(WM_NOTIFY_DOCK, (WPARAM)m_bIsDocked, (LPARAM)m_nDockSide);
	}
	BOOL IsAeroEnabled()
	{
		HRESULT aResult = S_OK;
		BOOL isEnabled = FALSE;

		// Load the dll and keep the handle to it
		// Must load dynamically because this dll exists only in vista and newer
		HMODULE dwmapiDllHandle = LoadLibrary(L"dwmapi.dll");
		
		if (dwmapiDllHandle != NULL)	// not on XP, so do aero calculations
		{
			DwmIsCompositionEnabledFunc DwmIsCompositionEnabled;
			DwmIsCompositionEnabled = (DwmIsCompositionEnabledFunc)
				::GetProcAddress(dwmapiDllHandle, "DwmIsCompositionEnabled");
			if (DwmIsCompositionEnabled != NULL)
			{
				aResult = DwmIsCompositionEnabled(&isEnabled);
			}
			if (!SUCCEEDED(aResult))
				isEnabled = FALSE;
		}
		FreeLibrary(dwmapiDllHandle);
		return isEnabled;
	}
	int GetAeroOffset()
	{
		const int DIVISOR = -15;
		int retVal = AERO_OFFSET;
		HKEY m_hKey, m_base = HKEY_CURRENT_USER;
		CString m_value, m_key = _T("PaddedBorderWidth"), m_path = _T("Control Panel\\Desktop\\WindowMetrics");
		if (RegOpenKeyEx(m_base, m_path, 0, KEY_EXECUTE, &m_hKey) == ERROR_SUCCESS)
		{
			int size = 0;
			DWORD type;
			RegQueryValueEx(m_hKey, m_key, NULL, &type, NULL, (LPDWORD)&size);
			TCHAR* pStr = new TCHAR[size];
			if (RegQueryValueEx(m_hKey, m_key, NULL, &type, (BYTE*)pStr, (LPDWORD)&size) == ERROR_SUCCESS)
			{
				m_value = CString(pStr);
				delete [] pStr;
				ASSERT(type == REG_SZ);
				RegCloseKey(m_hKey);
				int retVal = _ttoi(m_value);
				return (retVal / DIVISOR);
			}
			else
			{
				delete [] pStr;
			}
		}
		RegCloseKey(m_hKey);
		return retVal;
	}
}; // class CExtCS : public CExtCSBase

#endif // _CEXTWS_H_

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Systems / Hardware Administrator
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions