File: System\Windows\Input\Cursor.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.ComponentModel;
using MS.Win32;
using System.Runtime.InteropServices;
using System.IO;
 
namespace System.Windows.Input
{
    /// <summary>
    ///     Cursor class to support default cursor types.
    ///    TBD: Support for cutomized cursor types.
    /// </summary>
    [TypeConverter(typeof(CursorConverter))]
    [Localizability(LocalizationCategory.None, Readability = Readability.Unreadable)]
    public sealed class Cursor : IDisposable
    {
        /// <summary>
        /// Constructor for Standard Cursors, needn't be public as Stock Cursors
        /// are exposed in Cursors clas.
        /// </summary>
        /// <param name="cursorType"></param>
        internal Cursor(CursorType cursorType)
        {
            if (IsValidCursorType(cursorType))
            {
                LoadCursorHelper(cursorType);
            }
            else
            {
                throw new ArgumentException(SR.Format(SR.InvalidCursorType, cursorType));
            }
        }
 
        /// <summary>
        /// Cursor from .ani or .cur file
        /// </summary>
        /// <param name="cursorFile"></param>
        public Cursor(string cursorFile):this(cursorFile, false)
        {
        }
 
        /// <summary>
        /// Cursor from .ani or .cur file
        /// </summary>
        /// <param name="cursorFile"></param>
        /// <param name="scaleWithDpi"></param>
        public Cursor(string cursorFile, bool scaleWithDpi)
        {
            _scaleWithDpi = scaleWithDpi;
            ArgumentNullException.ThrowIfNull(cursorFile);
 
            if ((cursorFile != String.Empty) &&
                (cursorFile.EndsWith(".cur", StringComparison.OrdinalIgnoreCase) ||
                 cursorFile.EndsWith(".ani", StringComparison.OrdinalIgnoreCase)))
            {
                LoadFromFile(cursorFile);
                _fileName = cursorFile;
            }
            else
            {
                throw new ArgumentException(SR.Format(SR.Cursor_UnsupportedFormat , cursorFile));
            }
        }
 
        /// <summary>
        /// Cursor from Stream
        /// </summary>
        /// <param name="cursorStream"></param>
        public Cursor(Stream cursorStream):this(cursorStream, false)
        {
        }
 
        /// <summary>
        /// Cursor from Stream
        /// </summary>
        /// <param name="cursorStream"></param>
        /// <param name="scaleWithDpi"></param>
        public Cursor(Stream cursorStream, bool scaleWithDpi)
        {
            _scaleWithDpi = scaleWithDpi;
            ArgumentNullException.ThrowIfNull(cursorStream);
            LoadFromStream(cursorStream);
        }
 
        /// <summary>
        ///     Cursor from a SafeHandle to an HCURSOR
        /// </summary>
        /// <param name="cursorHandle"></param>
        internal Cursor(SafeHandle cursorHandle )
        {
            if (! cursorHandle.IsInvalid )
            {
                this._cursorHandle = cursorHandle ;
            }
        }
 
         /// <summary>
        /// Destructor (IDispose pattern)
        /// </summary>
        ~Cursor()
        {
            Dispose(false);
        }
 
        ///  <summary>
        ///     Cleans up the resources allocated by this object.  Once called, the cursor
        ///     object is no longer useful.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        void Dispose(bool disposing)
        {
            if ( _cursorHandle != null )
            {
                _cursorHandle.Dispose();
                _cursorHandle = null;
            }
        }
 
 
 
        /// <summary>
        /// CursorType - Cursor Type Enumeration
        /// </summary>
        /// <value></value>
        internal CursorType CursorType
        {
            get
            {
                return _cursorType;
            }
        }
 
        /// <summary>
        /// Handle - HCURSOR Interop
        /// </summary>
        /// <value></value>
        internal SafeHandle Handle
        {
            get
            {
                return _cursorHandle ?? NativeMethods.CursorHandle.GetInvalidCursor();
            }
        }
 
 
        /// <summary>
        /// FileName - .ani or .cur files are allowed
        /// </summary>
        /// <value></value>
        internal String FileName
        {
            get
            {
                return _fileName;
            }
        }
 
        private void LoadFromFile(string fileName)
        {
            // Load a Custom Cursor
            _cursorHandle = UnsafeNativeMethods.LoadImageCursor(IntPtr.Zero,
                                                                fileName,
                                                                NativeMethods.IMAGE_CURSOR,
                                                                0, 0,
                                                                NativeMethods.LR_DEFAULTCOLOR |
                                                                NativeMethods.LR_LOADFROMFILE |
                                                                (_scaleWithDpi? NativeMethods.LR_DEFAULTSIZE : 0x0000));
 
            int errorCode = Marshal.GetLastWin32Error();
            if (_cursorHandle == null || _cursorHandle.IsInvalid)
            {
                // LoadImage returns a null handle but does not set
                // the error condition when icon file is of an incorrect type (e.g., .bmp)
                //
                // LoadImage has a bug where it doesn't set the correct error code
                // when a file is given that is not an ico file.  Icon load fails
                // but win32 error code is still zero (success).  Thus, we need to
                // special case this scenario.
                //
                if (errorCode != 0)
                {
                    if ((errorCode == NativeMethods.ERROR_FILE_NOT_FOUND) || (errorCode == NativeMethods.ERROR_PATH_NOT_FOUND))
                    {
                        throw new Win32Exception(errorCode, SR.Format(SR.Cursor_LoadImageFailure, fileName));
                    }
                    else
                    {
                        throw new Win32Exception(errorCode);
                    }
                }
                else
                {
                    throw new ArgumentException(SR.Format(SR.Cursor_LoadImageFailure, fileName));
                }
            }
        }
 
        //**** DEAD CODE - retained only for compat, if user sets quirk flag  ****
        private const int BUFFERSIZE = 4096; // the maximum size of the buffer used for loading from stream
 
        private void LegacyLoadFromStream(Stream cursorStream)
        {
            //Generate a temporal file based on the memory stream.
 
            // GetTempFileName requires unrestricted Environment permission
            // FileIOPermission.Write permission.  However, since we don't
            // know the path of the file to be created we have to give
            // unrestricted permission here.
            // GetTempFileName documentation does not mention that it throws
            // any exception. However, if it does, CLR reverts the assert.
 
            string filePath = Path.GetTempFileName();
            try
            {
                using (BinaryReader reader = new BinaryReader(cursorStream))
                {
                    using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.None))
                    {
                        // Read the bytes from the stream, up to BUFFERSIZE
                        byte[] cursorData = reader.ReadBytes(BUFFERSIZE);
                        int dataSize;
 
                        // If the buffer is filled up, then write those bytes out and read more bytes up to BUFFERSIZE
                        for (dataSize = cursorData.Length;
                             dataSize >= BUFFERSIZE;
                             dataSize = reader.Read(cursorData, 0 /*index in array*/, BUFFERSIZE /*bytes to read*/))
                        {
                            fileStream.Write(cursorData, 0 /*index in array*/, BUFFERSIZE /*bytes to write*/);
                        }
 
                        // Write any remaining bytes
                        fileStream.Write(cursorData, 0 /*index in array*/, dataSize /*bytes to write*/);
                    }
                }
 
                // This method is called with File Write permission still asserted.
                // However, this method just reads this file into an icon.
                _cursorHandle = UnsafeNativeMethods.LoadImageCursor(IntPtr.Zero,
                                                                    filePath,
                                                                    NativeMethods.IMAGE_CURSOR,
                                                                    0, 0,
                                                                    NativeMethods.LR_DEFAULTCOLOR |
                                                                    NativeMethods.LR_LOADFROMFILE |
                                                                    (_scaleWithDpi? NativeMethods.LR_DEFAULTSIZE : 0x0000));
                if (_cursorHandle == null || _cursorHandle.IsInvalid)
                {
                     throw new ArgumentException(SR.Cursor_InvalidStream);
                }
            }
            finally
            {
                try
                {
                    File.Delete(filePath);
                }
                catch(System.IO.IOException)
                {
                    //  We may not be able to delete the file if it's being used by some other process (e.g. Anti-virus check).
                    // There's nothing we can do in that case, so just eat the exception and leave the file behind
                }
            }
        }
        //**** end of DEAD CODE ****//
 
        private void LoadFromStream(Stream cursorStream)
        {
            if (MS.Internal.CoreAppContextSwitches.AllowExternalProcessToBlockAccessToTemporaryFiles)
            {
                LegacyLoadFromStream(cursorStream);
                return;
            }
 
            string filePath = null;
 
            try
            {
                // Generate a temporary file based on the memory stream.
                using (FileStream fileStream = FileHelper.CreateAndOpenTemporaryFile(out filePath))
                {
                    cursorStream.CopyTo(fileStream);
                }
 
                // create a cursor from the temp file
                _cursorHandle = UnsafeNativeMethods.LoadImageCursor(IntPtr.Zero,
                                                                    filePath,
                                                                    NativeMethods.IMAGE_CURSOR,
                                                                    0, 0,
                                                                    NativeMethods.LR_DEFAULTCOLOR |
                                                                    NativeMethods.LR_LOADFROMFILE |
                                                                    (_scaleWithDpi? NativeMethods.LR_DEFAULTSIZE : 0x0000));
                if (_cursorHandle == null || _cursorHandle.IsInvalid)
                {
                     throw new ArgumentException(SR.Cursor_InvalidStream);
                }
            }
            finally
            {
                FileHelper.DeleteTemporaryFile(filePath);
            }
        }
 
        private void LoadCursorHelper(CursorType cursorType)
        {
            if (cursorType != CursorType.None)
            {
                // Load a Standard Cursor
                _cursorHandle = SafeNativeMethods.LoadCursor(new HandleRef(this,IntPtr.Zero), (IntPtr)(CursorTypes[(int)cursorType]));
            }
            this._cursorType = cursorType;
        }
 
        /// <summary>
        /// String Output
        /// </summary>
        /// <remarks>
        /// Remove this and let users use CursorConverter.ConvertToInvariantString() method
        /// </remarks>
        public override string ToString()
        {
            if (_fileName != String.Empty)
                return _fileName;
            else
            {
                // Get the string representation fo the cursor type enumeration.
                return Enum.GetName(_cursorType);
            }
        }
 
        private bool IsValidCursorType(CursorType cursorType)
        {
            return ((int)cursorType >= (int)CursorType.None && (int)cursorType <= (int)CursorType.ArrowCD);
        }
 
        private string      _fileName     = String.Empty;
        private CursorType  _cursorType   = CursorType.None;
        private bool        _scaleWithDpi = false;
 
        private SafeHandle  _cursorHandle;
 
        private static ReadOnlySpan<int> CursorTypes => [
            0, // None
            NativeMethods.IDC_NO,
            NativeMethods.IDC_ARROW,
            NativeMethods.IDC_APPSTARTING,
            NativeMethods.IDC_CROSS,
            NativeMethods.IDC_HELP,
            NativeMethods.IDC_IBEAM,
            NativeMethods.IDC_SIZEALL,
            NativeMethods.IDC_SIZENESW,
            NativeMethods.IDC_SIZENS,
            NativeMethods.IDC_SIZENWSE,
            NativeMethods.IDC_SIZEWE,
            NativeMethods.IDC_UPARROW,
            NativeMethods.IDC_WAIT,
            NativeMethods.IDC_HAND,
            NativeMethods.IDC_ARROW + 119, // PenCursor
            NativeMethods.IDC_ARROW + 140, // ScrollNSCursor
            NativeMethods.IDC_ARROW + 141, // ScrollWECursor
            NativeMethods.IDC_ARROW + 142, // ScrollAllCursor
            NativeMethods.IDC_ARROW + 143, // ScrollNCursor
            NativeMethods.IDC_ARROW + 144, // ScrollSCursor
            NativeMethods.IDC_ARROW + 145, // ScrollWCursor
            NativeMethods.IDC_ARROW + 146, // ScrollECursor
            NativeMethods.IDC_ARROW + 147, // ScrollNWCursor
            NativeMethods.IDC_ARROW + 148, // ScrollNECursor
            NativeMethods.IDC_ARROW + 149, // ScrollSWCursor
            NativeMethods.IDC_ARROW + 150, // ScrollSECursor
            NativeMethods.IDC_ARROW + 151 // ArrowCDCursor
       ];
    }
}