Thursday, December 04, 2014

Programatically Reordering Excel Columns

My task is to reorder the columns in an Excel spreadsheet.  The files are generally quite large (75-100K rows, up to 200 columns).

My first inclination was, as usual, EPPlus.  It took a bit of doing, but I got it working.  Only it was slow.  Really slow.  It will stream properly and not overload memory, but too slow.

After reading this post that mentioned the benefits of using Excel automation, I thought I'd give that a try.  I was amazed.  Even though I'm not actually saving the resulting file until after I've copy/pasted all columns of data in, I never see the memory spike.  There's some really good memory management in there.  Yes, having a dependency on a COM object isn't optimal, but when we're talking Excel, I think it's acceptable.  It freakin rocks, and the code is very simple.

Anyway, I'm including both here (if you have a need for this and your files aren't large, you might want to use EPPlus).

In either case the call is like:
EPPlus version:

Helpers:
Main Class:
Excel Automation version:

Wednesday, January 08, 2014

MP3 Player Hack (one way Windows 8 on a tablet is so useful)

I have a few requirements for an MP3 player on my tablet:
  • I need playlists.  Pretty basic.
  • I need a large area that, when tapped, will pause play.
  • Another fairly large area that will rewind 15-30 seconds.
  • I need the app to remember where I left off and resume there if I close/reopen.
  • I need the dang thing not to shut off or for the screen to go black disallowing input.
The reasons for these is because I listen to lectures while driving (with the tablet connected to the car audio).  When I want to pause, I need to be able to simply reach over and tap darn near anywhere to pause.  I need to rewind 30 for when I didn't quite catch part of the lecture.

After quite a search, I couldn't find anything in the windows store that worked (one app came close, had those features, but simply wigged out whenever resuming play after turning the tablet back on).  I probably could find something in the google or apple stores that worked, but for myriad other reasons, I love my windows tablet.

Legacy windows to the rescue.  I can run legacy windows applications, so surely the ever-loved, classic, most fully featured MP3 player in history; WinAmp will fill my needs, right?  Well, it came really close.  Of course, it installed and worked fine on the legacy side of Windows 8, which right there is awesome.  The other vendor's tablets just aren't going to run a 15 year old (or however old WinAmp is) application.

Close.  Prefs allowed me to zoom the interface (using the "modern" skin) so the buttons became large.  Pause is pretty easy to find and push now.  Amazingly, WinAmp doesn't remember where you left off (unless it's a podcast) so I needed a plug-in for that.  Now I'm close.  I need it to keep the screen on (not sure there's a plug-in for that), and I still can't rewind.  The closest way is to invoke the on-screen keyboard and hit the back arrow.

Simple.  I want a controller for WinAmp.  I want something that starts WinAmp (allowing it to simply resume where it left off), issues 2 keystroke commands to it (pause, rewind), and prevents the tablet from going to sleep.  So I created that in very little time.  I threw together an old-school WinForms (damn that technology was useful in many situations, wasn't it?) app that starts WinAmp when it starts, shows itself full-screen with ~ 75% of it's surface for pause, the rest for rewind, and sends a mouse move command every 2 minutes to prevent screen shutdown.  Ye olde Win32 API is ugly, but having the ability to still use it is pretty cool.
If someone out there actually wants the executable, contact me and I'll send it.

The Win32 Code.

  1. class Win32Trash
  2. {
  3.     [DllImport( "User32" )]
  4.     public static extern int SetForegroundWindow( IntPtr hwnd );
  5.  
  6.     /// <summary>
  7.     /// Synthesizes keystrokes, mouse motions, and button clicks.
  8.     /// </summary>
  9.     [DllImport( "User32.dll" )]
  10.     public static extern uint SendInput( uint numberOfInputs, [MarshalAs( UnmanagedType.LPArray, SizeConst = 1 )] INPUT[] input, int structSize );
  11.  
  12.     public enum InputType
  13.     {
  14.         INPUT_MOUSE = 0,
  15.         INPUT_KEYBOARD = 1,
  16.         INPUT_HARDWARE = 2
  17.     };
  18.  
  19.     public enum MouseEventFlags
  20.     {
  21.         MOVE = 0x00000001,
  22.         LEFTDOWN = 0x00000002,
  23.         LEFTUP = 0x00000004,
  24.         RIGHTDOWN = 0x00000008,
  25.         MIDDLEDOWN = 0x00000020,
  26.         MIDDLEUP = 0x00000040,
  27.         ABSOLUTE = 0x00008000,
  28.         RIGHTUP = 0x00000010
  29.     };
  30.  
  31.     public struct MOUSEINPUT
  32.     {
  33.         public int dx;
  34.         public int dy;
  35.         public int mouseData;
  36.         public MouseEventFlags dwFlags;
  37.         public int time;
  38.         public IntPtr dwExtraInfo;
  39.     };
  40.  
  41.     public struct KEYBDINPUT
  42.     {
  43.         public short wVk;
  44.         public short wScan;
  45.         public int dwFlags;
  46.         public int time;
  47.         public IntPtr dwExtraInfo;
  48.     };
  49.  
  50.     public struct HARDWAREINPUT
  51.     {
  52.         public int uMsg;
  53.         public short wParamL;
  54.         public short wParamH;
  55.     };
  56.  
  57.     [StructLayout(LayoutKind.Explicit)]
  58.     public struct MOUSEKEYBDHARDWAREINPUT
  59.     {
  60.         [FieldOffset(0)]
  61.         public MOUSEINPUT mi;
  62.         [FieldOffset( 0 )]
  63.         public KEYBDINPUT ki;
  64.         [FieldOffset( 0 )]
  65.         public HARDWAREINPUT hi;
  66.     };
  67.  
  68.     public struct INPUT
  69.     {
  70.         public InputType dwType;
  71.         public MOUSEKEYBDHARDWAREINPUT mkhi;
  72.     };
  73.  
  74. }

The WinFormCode.  Set the form state to maximized.  There is a timer hard coded to fire every 2 minutes (to issue the mouse move command to prevent tablet sleep), and the path to WinAmp is hard coded to where it's installed by default.

  1. public partial class WinAmpController : Form
  2. {
  3.  
  4.     private static Win32Trash.INPUT[] _junkForMouseInput = CreateMouseInput();
  5.     private static Process _winampProcess;
  6.  
  7.     public WinAmpController()
  8.     {
  9.         InitializeComponent();
  10.     }
  11.  
  12.     public void launchWinAmp()
  13.     {
  14.         try
  15.         {
  16.             // We don't want to attach to an existing process if one exists
  17.             var si = new ProcessStartInfo( @"C:\Program Files\Winamp\winamp.exe" ) { UseShellExecute = false, WindowStyle = ProcessWindowStyle.Normal };
  18.             _winampProcess = Process.Start( si );
  19.             _winampProcess.WaitForInputIdle();
  20.         }
  21.         catch( Exception e )
  22.         {
  23.             MessageBox.Show( "Error launching Winamp: " + e.Message );
  24.         }
  25.  
  26.     }
  27.  
  28.     private void WinAmpController_Load( object sender, EventArgs e )
  29.     {
  30.         launchWinAmp();
  31.         /// Expected that the plug-in will begin playing where left off.
  32.         this.TopMost = true;
  33.     }
  34.  
  35.     private void WinAmpController_FormClosing( object sender, FormClosingEventArgs e )
  36.     {
  37.         if ( _winampProcess != null )
  38.         {
  39.             _winampProcess.CloseMainWindow();
  40.             _winampProcess.Close();
  41.         }
  42.     }
  43.  
  44.     private static void SendKey( string key )
  45.     {
  46.         IntPtr windowHandle = _winampProcess.MainWindowHandle;
  47.         //ShowWindow( windowHandle, 1 );  // ain't shit
  48.         Win32Trash.SetForegroundWindow( windowHandle );
  49.         SendKeys.SendWait( key );
  50.     }
  51.  
  52.     private void btnPause_Click( object sender, EventArgs e )
  53.     {
  54.         SendKey("c");
  55.     }
  56.  
  57.     /// <summary>
  58.     /// Each left command is only 5 secs back.  This results in about 19 secs back.  Putting multiple calls to SendWait inside the SendKey method didn't work.
  59.     /// </summary>
  60.     /// <param name="sender"></param>
  61.     /// <param name="e"></param>
  62.     private void btnBack_Click( object sender, EventArgs e )
  63.     {
  64.         SendKey( "{LEFT}" );
  65.         SendKey( "{LEFT}" );
  66.         SendKey( "{LEFT}" );
  67.     }
  68.  
  69.     /// <summary>
  70.     /// Set for every 2 min.
  71.     /// </summary>
  72.     /// <param name="sender"></param>
  73.     /// <param name="e"></param>
  74.     private void timer1_Tick( object sender, EventArgs e )
  75.     {
  76.         this.Text = "timer clicked";
  77.         MoveMouse();
  78.     }
  79.  
  80.     private static Win32Trash.INPUT[] CreateMouseInput()
  81.     {
  82.         var i = new Win32Trash.INPUT()
  83.         {
  84.             dwType = Win32Trash.InputType.INPUT_MOUSE,
  85.             mkhi = new Win32Trash.MOUSEKEYBDHARDWAREINPUT()
  86.             {
  87.                 mi = new Win32Trash.MOUSEINPUT()
  88.                 {
  89.                     dx = 0,
  90.                     dy = 0,
  91.                     mouseData = 0,
  92.                     dwFlags = Win32Trash.MouseEventFlags.MOVE,
  93.                     time = 0,
  94.                     dwExtraInfo = IntPtr.Zero
  95.                 }
  96.             }
  97.         };
  98.         return new Win32Trash.INPUT[] { i };
  99.     }
  100.  
  101.     private void MoveMouse()
  102.     {
  103.         Win32Trash.SendInput( 1, _junkForMouseInput, Marshal.SizeOf( _junkForMouseInput[0] ) );
  104.     }
  105.  
  106. }