Recently, I have made a fork of Arduino LiquidCrystal (HD44780 display driver library) that allows the library to work with the IO abstraction library, meaning you can configure a display to use Arduino pins, an i2c 8574 IO expander or shift registers by simply changing one line of code in your sketch.
There are two additional examples provided with this version that show how to use the fork with both a shift register and an 8574 i2c IO expander. If the library is configured without an abstraction being provided, it defaults to Arduino pins, just like it used to do.
Download link: LiquidCrystalIO library on github
Usage is exactly the same as the standard version of the library with a minor caveat. You must call the liquid crystal begin method from within the setup() method, which is not mandatory in the regular version, but must be done in this version. Other than this very minor detail, the library should work without any other changes.
This library also uses TaskManger, making it completely compatible with the TaskManager framework, it ensures that any significant waits for the display hardware are done through task manager, to avoid potentially 100s of micros of latency. It does however mean that you should do all rendering in a single repeating task when using with task manager.
Another advantage of this library is that it can be programmed to work with any i2c pinout be it PCF8574 or MCP23017.
As per the diagram above, but use a shift register instead of the i2c device.